feat(v2): auto-generated sidebars, frontmatter-less sites (#4582)

* POC of autogenerated sidebars

* use combine-promises utility lib

* autogenerated sidebar poc working

* Revert "autogenerated sidebar poc working"

This reverts commit c81da980

* POC of auto-generated sidebars for community docs

* update tests

* add initial test suite for autogenerated sidebars + fix some edge cases

* Improve autogen sidebars: strip more number prefixes in folder breadcrumb + slugs

* fix typo!

* Add tests for partially generated sidebars + fix edge cases + extract sidebar generation code

* Ability to read category metadatas file from a file in the category

* fix tests

* change position of API

* ability to extract number prefix

* stable system to enable position frontmatter

* fix tests for autogen sidebar position

* renamings

* restore community sidebars

* rename frontmatter position -> sidebar_position

* make sidebarItemsGenerator fn configurable

* minor changes

* rename dirPath => dirName

* Make the init template use autogenerated sidebars

* fix options

* fix docusaurus site: remove test docs

* add _category_ file to docs pathsToWatch

* add _category_ file to docs pathsToWatch

* tutorial: use sidebar_position instead of file number prefixes

* Adapt Docusaurus tutorial for autogenerated sidebars

* remove slug: /

* polish the homepage template

* rename _category_ sidebar_position to just "position"

* test for custom sidebarItemsGenerator fn

* fix category metadata + add link to report tutorial issues

* fix absolute path breaking tests

* fix absolute path breaking tests

* Add test for floating number sidebar_position

* add sidebarItemsGenerator unit tests

* add processSidebars unit tests

* Fix init template broken links

* windows test

* increase code translations test timeout

* cleanup mockCategoryMetadataFiles after windows test fixed

* update init template positions

* fix windows tests

* fix comment

* Add autogenerated sidebar items documentation + rewrite the full sidebars page doc

* add useful comment

* fix code block title
This commit is contained in:
Sébastien Lorber 2021-04-15 16:20:11 +02:00 committed by GitHub
parent 836f92708a
commit db79d462ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
67 changed files with 2887 additions and 306 deletions

View file

@ -1,38 +0,0 @@
---
title: Create a Document
---
Documents are a **group of pages** connected through a **sidebar**, a **previous/next navigation** and **versioning**.
## Create a Document
Create a markdown file at `docs/my-doc.md`:
```mdx title="docs/hello.md"
---
title: Hello, World!
---
## Hello, World!
This is your first document in **Docusaurus**, Congratulations!
```
A new document is now available at `http://localhost:3000/docs/hello`.
## Add your document to the sidebar
Add `hello` to the `sidebars.js` file:
```diff title="sidebars.js"
module.exports = {
docs: [
{
type: 'category',
label: 'Docusaurus Tutorial',
- items: ['getting-started', 'create-a-doc', ...],
+ items: ['getting-started', 'create-a-doc', 'hello', ...],
},
],
};
```

View file

@ -1,11 +1,16 @@
--- ---
title: Getting Started sidebar_position: 1
slug: /
--- ---
Get started by **creating a new site** # Tutorial Intro
Or **try Docusaurus immediately** with **[new.docusaurus.io](https://new.docusaurus.io)** (CodeSandbox). Let's discover **Docusaurus in less than 5 minutes**.
## Getting Started
Get started by **creating a new site**.
Or **try Docusaurus immediately** with **[new.docusaurus.io](https://new.docusaurus.io)**.
## Generate a new site ## Generate a new site

View file

@ -0,0 +1,4 @@
{
"label": "Tutorial - Basics",
"position": 2
}

View file

@ -1,14 +1,16 @@
--- ---
title: Congratulations! sidebar_position: 6
--- ---
Congratulations on making it this far! # Congratulations!
You have learned the **basics of Docusaurus** and made some changes to the **initial template**. You have just learned the **basics of Docusaurus** and made some changes to the **initial template**.
Docusaurus has **much more to offer**! Docusaurus has **much more to offer**!
Have 5 more minutes? Take a look at **[versioning](./manage-docs-versions.md)** and **[i18n](./translate-your-site.md)**. Have **5 more minutes**? Take a look at **[versioning](../tutorial-extras/manage-docs-versions.md)** and **[i18n](../tutorial-extras/translate-your-site.md)**.
Anything **unclear** or **buggy** in this tutorial? [Please report it!](https://github.com/facebook/docusaurus/discussions/4610)
## What's next? ## What's next?

View file

@ -1,10 +1,12 @@
--- ---
title: Create a Blog Post sidebar_position: 3
--- ---
# Create a Blog Post
Docusaurus creates a **page for each blog post**, but also a **blog index page**, a **tag system**, an **RSS** feed... Docusaurus creates a **page for each blog post**, but also a **blog index page**, a **tag system**, an **RSS** feed...
## Create a Blog Post ## Create your first Post
Create a file at `blog/2021-02-28-greetings.md`: Create a file at `blog/2021-02-28-greetings.md`:

View file

@ -0,0 +1,56 @@
---
sidebar_position: 2
---
# Create a Document
Documents are **groups of pages** connected through:
- a **sidebar**
- **previous/next navigation**
- **versioning**
## Create your first Doc
Create a markdown file at `docs/hello.md`:
```md title="docs/hello.md"
# Hello
This is my **first Docusaurus document**!
```
A new document is now available at `http://localhost:3000/docs/hello`.
## Configure the Sidebar
Docusaurus automatically **creates a sidebar** from the `docs` folder.
Add metadatas to customize the sidebar label and position:
```diff title="docs/hello.md"
+ ---
+ sidebar_label: "Hi!"
+ sidebar_position: 3
+ ---
# Hello
This is my **first Docusaurus document**!
```
It is also possible to create your sidebar explicitly in `sidebars.js`:
```diff title="sidebars.js"
module.exports = {
tutorialSidebar: [
{
type: 'category',
label: 'Tutorial',
- items: [...],
+ items: ['hello'],
},
],
};
```

View file

@ -1,14 +1,16 @@
--- ---
title: Create a Page sidebar_position: 1
--- ---
Add **Markdown or React** files to `src/pages` to create **standalone pages**: # Create a Page
Add **Markdown or React** files to `src/pages` to create a **standalone page**:
- `src/pages/index.js` -> `localhost:3000/` - `src/pages/index.js` -> `localhost:3000/`
- `src/pages/foo.md` -> `localhost:3000/foo` - `src/pages/foo.md` -> `localhost:3000/foo`
- `src/pages/foo/bar.js` -> `localhost:3000/foo/bar` - `src/pages/foo/bar.js` -> `localhost:3000/foo/bar`
## Create a React Page ## Create your first React Page
Create a file at `src/pages/my-react-page.js`: Create a file at `src/pages/my-react-page.js`:
@ -28,15 +30,11 @@ export default function MyReactPage() {
A new page is now available at `http://localhost:3000/my-react-page`. A new page is now available at `http://localhost:3000/my-react-page`.
## Create a Markdown Page ## Create your first Markdown Page
Create a file at `src/pages/my-markdown-page.md`: Create a file at `src/pages/my-markdown-page.md`:
```mdx title="src/pages/my-markdown-page.md" ```mdx title="src/pages/my-markdown-page.md"
---
title: My Markdown page
---
# My Markdown page # My Markdown page
This is a Markdown page This is a Markdown page

View file

@ -1,8 +1,12 @@
--- ---
title: Deploy your site sidebar_position: 5
--- ---
Docusaurus is a **static-site-generator** (also called [Jamstack](https://jamstack.org/)), and builds your site as **static HTML, JavaScript and CSS files**. # Deploy your site
Docusaurus is a **static-site-generator** (also called **[Jamstack](https://jamstack.org/)**).
It builds your site as simple **static HTML, JavaScript and CSS files**.
## Build your site ## Build your site
@ -12,7 +16,7 @@ Build your site **for production**:
npm run build npm run build
``` ```
The static files are generated in the `build` directory. The static files are generated in the `build` folder.
## Deploy your site ## Deploy your site

View file

@ -1,20 +1,24 @@
--- ---
title: Markdown Features sidebar_position: 4
--- ---
# Markdown Features
Docusaurus supports **[Markdown](https://daringfireball.net/projects/markdown/syntax)** and a few **additional features**. Docusaurus supports **[Markdown](https://daringfireball.net/projects/markdown/syntax)** and a few **additional features**.
## Front Matter ## Front Matter
Markdown documents have metadata at the very top called [Front Matter](https://jekyllrb.com/docs/front-matter/): Markdown documents have metadata at the top called [Front Matter](https://jekyllrb.com/docs/front-matter/):
```md ```text title="my-doc.md"
// highlight-start
--- ---
id: my-doc id: my-doc-id
title: My document title title: My document title
description: My document description description: My document description
sidebar_label: My doc slug: /my-custom-url
--- ---
// highlight-end
## Markdown heading ## Markdown heading

View file

@ -0,0 +1,4 @@
{
"label": "Tutorial - Extras",
"position": 3
}

View file

@ -1,7 +1,9 @@
--- ---
title: Manage Docs Versions sidebar_position: 1
--- ---
# Manage Docs Versions
Docusaurus can manage multiple versions of your docs. Docusaurus can manage multiple versions of your docs.
## Create a docs version ## Create a docs version
@ -12,7 +14,7 @@ Release a version 1.0 of your project:
npm run docusaurus docs:version 1.0 npm run docusaurus docs:version 1.0
``` ```
The `docs` directory is copied into `versioned_docs/version-1.0` and `versions.json` is created. The `docs` folder is copied into `versioned_docs/version-1.0` and `versions.json` is created.
Your docs now have 2 versions: Your docs now have 2 versions:

View file

@ -1,7 +1,9 @@
--- ---
title: Translate your site sidebar_position: 2
--- ---
# Translate your site
Let's translate `docs/getting-started.md` to French. Let's translate `docs/getting-started.md` to French.
## Configure i18n ## Configure i18n
@ -19,7 +21,7 @@ module.exports = {
## Translate a doc ## Translate a doc
Copy the `docs/getting-started.md` file to the `i18n/fr` directory: Copy the `docs/getting-started.md` file to the `i18n/fr` folder:
```bash ```bash
mkdir -p i18n/fr/docusaurus-plugin-content-docs/current/ mkdir -p i18n/fr/docusaurus-plugin-content-docs/current/
@ -39,7 +41,7 @@ npm run start -- --locale fr
Your localized site is accessible at `http://localhost:3000/fr/` and the `Getting Started` page is translated. Your localized site is accessible at `http://localhost:3000/fr/` and the `Getting Started` page is translated.
:::warning :::caution
In development, you can only use one locale at a same time. In development, you can only use one locale at a same time.

View file

@ -19,7 +19,7 @@ module.exports = {
items: [ items: [
{ {
type: 'doc', type: 'doc',
docId: 'getting-started', docId: 'intro',
position: 'left', position: 'left',
label: 'Tutorial', label: 'Tutorial',
}, },
@ -38,8 +38,8 @@ module.exports = {
title: 'Docs', title: 'Docs',
items: [ items: [
{ {
label: 'Getting Started', label: 'Tutorial',
to: '/docs/', to: '/docs/intro',
}, },
], ],
}, },

View file

@ -1,22 +1,26 @@
/**
* Creating a sidebar enables you to:
- create an ordered group of docs
- render a sidebar for each doc of that group
- provide next/previous navigation
The sidebars can be generated from the filesystem, or explicitly defined here.
Create as many sidebars as you want.
*/
module.exports = { module.exports = {
tutorial: [ // By default, Docusaurus generates a sidebar from the docs folder structure
tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
// But you can create a sidebar manually
/*
tutorialSidebar: [
{ {
type: 'category', type: 'category',
label: 'Tutorial - Basics', label: 'Tutorial',
items: [ items: ['hello'],
'getting-started',
'create-a-page',
'create-a-document',
'create-a-blog-post',
'markdown-features',
'deploy-your-site',
'congratulations',
],
},
{
type: 'category',
label: 'Tutorial - Extras',
items: ['manage-docs-versions', 'translate-your-site'],
}, },
], ],
*/
}; };

View file

@ -41,8 +41,10 @@ function Feature({Svg, title, description}) {
<div className="text--center"> <div className="text--center">
<Svg className={styles.featureSvg} alt={title} /> <Svg className={styles.featureSvg} alt={title} />
</div> </div>
<h3>{title}</h3> <div className="text--center padding-horiz--md">
<p>{description}</p> <h3>{title}</h3>
<p>{description}</p>
</div>
</div> </div>
); );
} }

View file

@ -14,8 +14,10 @@ function HomepageHeader() {
<h1 className="hero__title">{siteConfig.title}</h1> <h1 className="hero__title">{siteConfig.title}</h1>
<p className="hero__subtitle">{siteConfig.tagline}</p> <p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}> <div className={styles.buttons}>
<Link className="button button--secondary button--lg" to="/docs"> <Link
Get Started - Docusaurus Tutorial className="button button--secondary button--lg"
to="/docs/intro">
Docusaurus Tutorial - 5min
</Link> </Link>
</div> </div>
</div> </div>

View file

@ -20,6 +20,7 @@
"devDependencies": { "devDependencies": {
"@docusaurus/module-type-aliases": "2.0.0-alpha.72", "@docusaurus/module-type-aliases": "2.0.0-alpha.72",
"@types/picomatch": "^2.2.1", "@types/picomatch": "^2.2.1",
"@types/js-yaml": "^4.0.0",
"commander": "^5.1.0", "commander": "^5.1.0",
"picomatch": "^2.1.1" "picomatch": "^2.1.1"
}, },
@ -30,10 +31,12 @@
"@docusaurus/utils": "2.0.0-alpha.72", "@docusaurus/utils": "2.0.0-alpha.72",
"@docusaurus/utils-validation": "2.0.0-alpha.72", "@docusaurus/utils-validation": "2.0.0-alpha.72",
"chalk": "^4.1.0", "chalk": "^4.1.0",
"combine-promises": "^1.1.0",
"execa": "^5.0.0", "execa": "^5.0.0",
"fs-extra": "^9.1.0", "fs-extra": "^9.1.0",
"globby": "^11.0.2", "globby": "^11.0.2",
"import-fresh": "^3.2.2", "import-fresh": "^3.2.2",
"js-yaml": "^4.0.0",
"loader-utils": "^1.2.3", "loader-utils": "^1.2.3",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"remark-admonitions": "^1.2.1", "remark-admonitions": "^1.2.1",

View file

@ -0,0 +1,3 @@
{
"label": "API (label from _category_.json)"
}

View file

@ -0,0 +1,8 @@
---
id: guide2.5
sidebar_position: 2.5
---
# Guide 2.5
Guide 2.5 text

View file

@ -0,0 +1,7 @@
---
id: guide2
---
# Guide 2
Guide 2 text

View file

@ -0,0 +1,7 @@
---
id: guide4
---
# Guide 4
Guide 4 text

View file

@ -0,0 +1,7 @@
---
id: guide5
---
# Guide 5
Guide 5 text

View file

@ -0,0 +1,8 @@
---
id: guide3
sidebar_position: 3
---
# Guide 3
Guide 3 text

View file

@ -0,0 +1,8 @@
---
id: guide1
sidebar_position: 1
---
# Guide 1
Guide 1 text

View file

@ -0,0 +1,14 @@
/**
* 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.
*/
module.exports = {
title: 'My Site',
tagline: 'The tagline of my site',
url: 'https://your-docusaurus-test-site.com',
baseUrl: '/',
favicon: 'img/favicon.ico',
};

View file

@ -0,0 +1,23 @@
/**
* 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.
*/
module.exports = {
someSidebar: [
{type: 'doc', id: 'API/api-end'},
{
type: 'category',
label: 'Some category',
items: [
{type: 'doc', id: 'API/api-overview'},
{
type: 'autogenerated',
dirName: '3-API/02_Extension APIs',
},
],
},
],
};

View file

@ -149,6 +149,7 @@ Object {
\\"title\\": \\"Bar\\", \\"title\\": \\"Bar\\",
\\"description\\": \\"This is custom description\\", \\"description\\": \\"This is custom description\\",
\\"source\\": \\"@site/docs/foo/bar.md\\", \\"source\\": \\"@site/docs/foo/bar.md\\",
\\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/bar\\", \\"slug\\": \\"/foo/bar\\",
\\"permalink\\": \\"/docs/foo/bar\\", \\"permalink\\": \\"/docs/foo/bar\\",
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
@ -170,6 +171,7 @@ Object {
\\"title\\": \\"baz\\", \\"title\\": \\"baz\\",
\\"description\\": \\"Images\\", \\"description\\": \\"Images\\",
\\"source\\": \\"@site/docs/foo/baz.md\\", \\"source\\": \\"@site/docs/foo/baz.md\\",
\\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/bazSlug.html\\", \\"slug\\": \\"/foo/bazSlug.html\\",
\\"permalink\\": \\"/docs/foo/bazSlug.html\\", \\"permalink\\": \\"/docs/foo/bazSlug.html\\",
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
@ -195,6 +197,7 @@ Object {
\\"title\\": \\"My heading as title\\", \\"title\\": \\"My heading as title\\",
\\"description\\": \\"\\", \\"description\\": \\"\\",
\\"source\\": \\"@site/docs/headingAsTitle.md\\", \\"source\\": \\"@site/docs/headingAsTitle.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/headingAsTitle\\", \\"slug\\": \\"/headingAsTitle\\",
\\"permalink\\": \\"/docs/headingAsTitle\\", \\"permalink\\": \\"/docs/headingAsTitle\\",
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
@ -207,6 +210,7 @@ Object {
\\"title\\": \\"Hello, World !\\", \\"title\\": \\"Hello, World !\\",
\\"description\\": \\"Hi, Endilie here :)\\", \\"description\\": \\"Hi, Endilie here :)\\",
\\"source\\": \\"@site/docs/hello.md\\", \\"source\\": \\"@site/docs/hello.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/\\", \\"slug\\": \\"/\\",
\\"permalink\\": \\"/docs/\\", \\"permalink\\": \\"/docs/\\",
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
@ -227,6 +231,7 @@ Object {
\\"title\\": \\"ipsum\\", \\"title\\": \\"ipsum\\",
\\"description\\": \\"Lorem ipsum.\\", \\"description\\": \\"Lorem ipsum.\\",
\\"source\\": \\"@site/docs/ipsum.md\\", \\"source\\": \\"@site/docs/ipsum.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/ipsum\\", \\"slug\\": \\"/ipsum\\",
\\"permalink\\": \\"/docs/ipsum\\", \\"permalink\\": \\"/docs/ipsum\\",
\\"editUrl\\": null, \\"editUrl\\": null,
@ -242,6 +247,7 @@ Object {
\\"title\\": \\"lorem\\", \\"title\\": \\"lorem\\",
\\"description\\": \\"Lorem ipsum.\\", \\"description\\": \\"Lorem ipsum.\\",
\\"source\\": \\"@site/docs/lorem.md\\", \\"source\\": \\"@site/docs/lorem.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/lorem\\", \\"slug\\": \\"/lorem\\",
\\"permalink\\": \\"/docs/lorem\\", \\"permalink\\": \\"/docs/lorem\\",
\\"editUrl\\": \\"https://github.com/customUrl/docs/lorem.md\\", \\"editUrl\\": \\"https://github.com/customUrl/docs/lorem.md\\",
@ -258,6 +264,7 @@ Object {
\\"title\\": \\"rootAbsoluteSlug\\", \\"title\\": \\"rootAbsoluteSlug\\",
\\"description\\": \\"Lorem\\", \\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/rootAbsoluteSlug.md\\", \\"source\\": \\"@site/docs/rootAbsoluteSlug.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootAbsoluteSlug\\", \\"slug\\": \\"/rootAbsoluteSlug\\",
\\"permalink\\": \\"/docs/rootAbsoluteSlug\\", \\"permalink\\": \\"/docs/rootAbsoluteSlug\\",
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
@ -272,6 +279,7 @@ Object {
\\"title\\": \\"rootRelativeSlug\\", \\"title\\": \\"rootRelativeSlug\\",
\\"description\\": \\"Lorem\\", \\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/rootRelativeSlug.md\\", \\"source\\": \\"@site/docs/rootRelativeSlug.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootRelativeSlug\\", \\"slug\\": \\"/rootRelativeSlug\\",
\\"permalink\\": \\"/docs/rootRelativeSlug\\", \\"permalink\\": \\"/docs/rootRelativeSlug\\",
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
@ -286,6 +294,7 @@ Object {
\\"title\\": \\"rootResolvedSlug\\", \\"title\\": \\"rootResolvedSlug\\",
\\"description\\": \\"Lorem\\", \\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/rootResolvedSlug.md\\", \\"source\\": \\"@site/docs/rootResolvedSlug.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/hey/rootResolvedSlug\\", \\"slug\\": \\"/hey/rootResolvedSlug\\",
\\"permalink\\": \\"/docs/hey/rootResolvedSlug\\", \\"permalink\\": \\"/docs/hey/rootResolvedSlug\\",
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
@ -300,6 +309,7 @@ Object {
\\"title\\": \\"rootTryToEscapeSlug\\", \\"title\\": \\"rootTryToEscapeSlug\\",
\\"description\\": \\"Lorem\\", \\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/rootTryToEscapeSlug.md\\", \\"source\\": \\"@site/docs/rootTryToEscapeSlug.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootTryToEscapeSlug\\", \\"slug\\": \\"/rootTryToEscapeSlug\\",
\\"permalink\\": \\"/docs/rootTryToEscapeSlug\\", \\"permalink\\": \\"/docs/rootTryToEscapeSlug\\",
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
@ -314,6 +324,7 @@ Object {
\\"title\\": \\"absoluteSlug\\", \\"title\\": \\"absoluteSlug\\",
\\"description\\": \\"Lorem\\", \\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/slugs/absoluteSlug.md\\", \\"source\\": \\"@site/docs/slugs/absoluteSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/absoluteSlug\\", \\"slug\\": \\"/absoluteSlug\\",
\\"permalink\\": \\"/docs/absoluteSlug\\", \\"permalink\\": \\"/docs/absoluteSlug\\",
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
@ -328,6 +339,7 @@ Object {
\\"title\\": \\"relativeSlug\\", \\"title\\": \\"relativeSlug\\",
\\"description\\": \\"Lorem\\", \\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/slugs/relativeSlug.md\\", \\"source\\": \\"@site/docs/slugs/relativeSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/relativeSlug\\", \\"slug\\": \\"/slugs/relativeSlug\\",
\\"permalink\\": \\"/docs/slugs/relativeSlug\\", \\"permalink\\": \\"/docs/slugs/relativeSlug\\",
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
@ -342,6 +354,7 @@ Object {
\\"title\\": \\"resolvedSlug\\", \\"title\\": \\"resolvedSlug\\",
\\"description\\": \\"Lorem\\", \\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/slugs/resolvedSlug.md\\", \\"source\\": \\"@site/docs/slugs/resolvedSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/hey/resolvedSlug\\", \\"slug\\": \\"/slugs/hey/resolvedSlug\\",
\\"permalink\\": \\"/docs/slugs/hey/resolvedSlug\\", \\"permalink\\": \\"/docs/slugs/hey/resolvedSlug\\",
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
@ -356,6 +369,7 @@ Object {
\\"title\\": \\"tryToEscapeSlug\\", \\"title\\": \\"tryToEscapeSlug\\",
\\"description\\": \\"Lorem\\", \\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/slugs/tryToEscapeSlug.md\\", \\"source\\": \\"@site/docs/slugs/tryToEscapeSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/tryToEscapeSlug\\", \\"slug\\": \\"/tryToEscapeSlug\\",
\\"permalink\\": \\"/docs/tryToEscapeSlug\\", \\"permalink\\": \\"/docs/tryToEscapeSlug\\",
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
@ -646,6 +660,134 @@ Array [
] ]
`; `;
exports[`site with custom sidebar items generator sidebarItemsGenerator is called with appropriate data 1`] = `
Object {
"docs": Array [
Object {
"frontMatter": Object {},
"id": "API/Core APIs/Client API",
"sidebarPosition": 0,
"source": "@site/docs/3-API/01_Core APIs/0 --- Client API.md",
"sourceDirName": "3-API/01_Core APIs",
},
Object {
"frontMatter": Object {},
"id": "API/Core APIs/Server API",
"sidebarPosition": 1,
"source": "@site/docs/3-API/01_Core APIs/1 --- Server API.md",
"sourceDirName": "3-API/01_Core APIs",
},
Object {
"frontMatter": Object {},
"id": "API/Extension APIs/Plugin API",
"sidebarPosition": 0,
"source": "@site/docs/3-API/02_Extension APIs/0. Plugin API.md",
"sourceDirName": "3-API/02_Extension APIs",
},
Object {
"frontMatter": Object {},
"id": "API/Extension APIs/Theme API",
"sidebarPosition": 1,
"source": "@site/docs/3-API/02_Extension APIs/1. Theme API.md",
"sourceDirName": "3-API/02_Extension APIs",
},
Object {
"frontMatter": Object {},
"id": "API/api-end",
"sidebarPosition": 3,
"source": "@site/docs/3-API/03_api-end.md",
"sourceDirName": "3-API",
},
Object {
"frontMatter": Object {},
"id": "API/api-overview",
"sidebarPosition": 0,
"source": "@site/docs/3-API/00_api-overview.md",
"sourceDirName": "3-API",
},
Object {
"frontMatter": Object {
"id": "guide1",
"sidebar_position": 1,
},
"id": "Guides/guide1",
"sidebarPosition": 1,
"source": "@site/docs/Guides/z-guide1.md",
"sourceDirName": "Guides",
},
Object {
"frontMatter": Object {
"id": "guide2",
},
"id": "Guides/guide2",
"sidebarPosition": 2,
"source": "@site/docs/Guides/02-guide2.md",
"sourceDirName": "Guides",
},
Object {
"frontMatter": Object {
"id": "guide2.5",
"sidebar_position": 2.5,
},
"id": "Guides/guide2.5",
"sidebarPosition": 2.5,
"source": "@site/docs/Guides/0-guide2.5.md",
"sourceDirName": "Guides",
},
Object {
"frontMatter": Object {
"id": "guide3",
"sidebar_position": 3,
},
"id": "Guides/guide3",
"sidebarPosition": 3,
"source": "@site/docs/Guides/guide3.md",
"sourceDirName": "Guides",
},
Object {
"frontMatter": Object {
"id": "guide4",
},
"id": "Guides/guide4",
"sidebarPosition": undefined,
"source": "@site/docs/Guides/a-guide4.md",
"sourceDirName": "Guides",
},
Object {
"frontMatter": Object {
"id": "guide5",
},
"id": "Guides/guide5",
"sidebarPosition": undefined,
"source": "@site/docs/Guides/b-guide5.md",
"sourceDirName": "Guides",
},
Object {
"frontMatter": Object {},
"id": "getting-started",
"sidebarPosition": 0,
"source": "@site/docs/0-getting-started.md",
"sourceDirName": ".",
},
Object {
"frontMatter": Object {},
"id": "installation",
"sidebarPosition": 1,
"source": "@site/docs/1-installation.md",
"sourceDirName": ".",
},
],
"item": Object {
"dirName": ".",
"type": "autogenerated",
},
"version": Object {
"contentPath": "docs",
"versionName": "current",
},
}
`;
exports[`site with wrong sidebar file 1`] = ` exports[`site with wrong sidebar file 1`] = `
"Bad sidebars file. "Bad sidebars file.
These sidebar document ids do not exist: These sidebar document ids do not exist:
@ -699,6 +841,7 @@ Object {
\\"title\\": \\"team\\", \\"title\\": \\"team\\",
\\"description\\": \\"Team 1.0.0\\", \\"description\\": \\"Team 1.0.0\\",
\\"source\\": \\"@site/community_versioned_docs/version-1.0.0/team.md\\", \\"source\\": \\"@site/community_versioned_docs/version-1.0.0/team.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/team\\", \\"slug\\": \\"/team\\",
\\"permalink\\": \\"/community/team\\", \\"permalink\\": \\"/community/team\\",
\\"version\\": \\"1.0.0\\", \\"version\\": \\"1.0.0\\",
@ -712,6 +855,7 @@ Object {
\\"title\\": \\"Team title translated\\", \\"title\\": \\"Team title translated\\",
\\"description\\": \\"Team current version (translated)\\", \\"description\\": \\"Team current version (translated)\\",
\\"source\\": \\"@site/i18n/en/docusaurus-plugin-content-docs-community/current/team.md\\", \\"source\\": \\"@site/i18n/en/docusaurus-plugin-content-docs-community/current/team.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/team\\", \\"slug\\": \\"/team\\",
\\"permalink\\": \\"/community/next/team\\", \\"permalink\\": \\"/community/next/team\\",
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
@ -942,6 +1086,7 @@ Object {
\\"title\\": \\"bar\\", \\"title\\": \\"bar\\",
\\"description\\": \\"This is next version of bar.\\", \\"description\\": \\"This is next version of bar.\\",
\\"source\\": \\"@site/docs/foo/bar.md\\", \\"source\\": \\"@site/docs/foo/bar.md\\",
\\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/barSlug\\", \\"slug\\": \\"/foo/barSlug\\",
\\"permalink\\": \\"/docs/next/foo/barSlug\\", \\"permalink\\": \\"/docs/next/foo/barSlug\\",
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
@ -961,6 +1106,7 @@ Object {
\\"title\\": \\"hello\\", \\"title\\": \\"hello\\",
\\"description\\": \\"Hello next !\\", \\"description\\": \\"Hello next !\\",
\\"source\\": \\"@site/docs/hello.md\\", \\"source\\": \\"@site/docs/hello.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/\\", \\"slug\\": \\"/\\",
\\"permalink\\": \\"/docs/next/\\", \\"permalink\\": \\"/docs/next/\\",
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
@ -978,6 +1124,7 @@ Object {
\\"title\\": \\"absoluteSlug\\", \\"title\\": \\"absoluteSlug\\",
\\"description\\": \\"Lorem\\", \\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/slugs/absoluteSlug.md\\", \\"source\\": \\"@site/docs/slugs/absoluteSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/absoluteSlug\\", \\"slug\\": \\"/absoluteSlug\\",
\\"permalink\\": \\"/docs/next/absoluteSlug\\", \\"permalink\\": \\"/docs/next/absoluteSlug\\",
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
@ -992,6 +1139,7 @@ Object {
\\"title\\": \\"relativeSlug\\", \\"title\\": \\"relativeSlug\\",
\\"description\\": \\"Lorem\\", \\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/slugs/relativeSlug.md\\", \\"source\\": \\"@site/docs/slugs/relativeSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/relativeSlug\\", \\"slug\\": \\"/slugs/relativeSlug\\",
\\"permalink\\": \\"/docs/next/slugs/relativeSlug\\", \\"permalink\\": \\"/docs/next/slugs/relativeSlug\\",
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
@ -1006,6 +1154,7 @@ Object {
\\"title\\": \\"resolvedSlug\\", \\"title\\": \\"resolvedSlug\\",
\\"description\\": \\"Lorem\\", \\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/slugs/resolvedSlug.md\\", \\"source\\": \\"@site/docs/slugs/resolvedSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/hey/resolvedSlug\\", \\"slug\\": \\"/slugs/hey/resolvedSlug\\",
\\"permalink\\": \\"/docs/next/slugs/hey/resolvedSlug\\", \\"permalink\\": \\"/docs/next/slugs/hey/resolvedSlug\\",
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
@ -1020,6 +1169,7 @@ Object {
\\"title\\": \\"tryToEscapeSlug\\", \\"title\\": \\"tryToEscapeSlug\\",
\\"description\\": \\"Lorem\\", \\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/docs/slugs/tryToEscapeSlug.md\\", \\"source\\": \\"@site/docs/slugs/tryToEscapeSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/tryToEscapeSlug\\", \\"slug\\": \\"/tryToEscapeSlug\\",
\\"permalink\\": \\"/docs/next/tryToEscapeSlug\\", \\"permalink\\": \\"/docs/next/tryToEscapeSlug\\",
\\"version\\": \\"current\\", \\"version\\": \\"current\\",
@ -1034,6 +1184,7 @@ Object {
\\"title\\": \\"hello\\", \\"title\\": \\"hello\\",
\\"description\\": \\"Hello 1.0.0 ! (translated en)\\", \\"description\\": \\"Hello 1.0.0 ! (translated en)\\",
\\"source\\": \\"@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md\\", \\"source\\": \\"@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/\\", \\"slug\\": \\"/\\",
\\"permalink\\": \\"/docs/1.0.0/\\", \\"permalink\\": \\"/docs/1.0.0/\\",
\\"version\\": \\"1.0.0\\", \\"version\\": \\"1.0.0\\",
@ -1051,6 +1202,7 @@ Object {
\\"title\\": \\"bar\\", \\"title\\": \\"bar\\",
\\"description\\": \\"Bar 1.0.0 !\\", \\"description\\": \\"Bar 1.0.0 !\\",
\\"source\\": \\"@site/versioned_docs/version-1.0.0/foo/bar.md\\", \\"source\\": \\"@site/versioned_docs/version-1.0.0/foo/bar.md\\",
\\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/barSlug\\", \\"slug\\": \\"/foo/barSlug\\",
\\"permalink\\": \\"/docs/1.0.0/foo/barSlug\\", \\"permalink\\": \\"/docs/1.0.0/foo/barSlug\\",
\\"version\\": \\"1.0.0\\", \\"version\\": \\"1.0.0\\",
@ -1070,6 +1222,7 @@ Object {
\\"title\\": \\"baz\\", \\"title\\": \\"baz\\",
\\"description\\": \\"Baz 1.0.0 ! This will be deleted in next subsequent versions.\\", \\"description\\": \\"Baz 1.0.0 ! This will be deleted in next subsequent versions.\\",
\\"source\\": \\"@site/versioned_docs/version-1.0.0/foo/baz.md\\", \\"source\\": \\"@site/versioned_docs/version-1.0.0/foo/baz.md\\",
\\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/baz\\", \\"slug\\": \\"/foo/baz\\",
\\"permalink\\": \\"/docs/1.0.0/foo/baz\\", \\"permalink\\": \\"/docs/1.0.0/foo/baz\\",
\\"version\\": \\"1.0.0\\", \\"version\\": \\"1.0.0\\",
@ -1091,6 +1244,7 @@ Object {
\\"title\\": \\"bar\\", \\"title\\": \\"bar\\",
\\"description\\": \\"Bar 1.0.1 !\\", \\"description\\": \\"Bar 1.0.1 !\\",
\\"source\\": \\"@site/versioned_docs/version-1.0.1/foo/bar.md\\", \\"source\\": \\"@site/versioned_docs/version-1.0.1/foo/bar.md\\",
\\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/bar\\", \\"slug\\": \\"/foo/bar\\",
\\"permalink\\": \\"/docs/foo/bar\\", \\"permalink\\": \\"/docs/foo/bar\\",
\\"version\\": \\"1.0.1\\", \\"version\\": \\"1.0.1\\",
@ -1108,6 +1262,7 @@ Object {
\\"title\\": \\"hello\\", \\"title\\": \\"hello\\",
\\"description\\": \\"Hello 1.0.1 !\\", \\"description\\": \\"Hello 1.0.1 !\\",
\\"source\\": \\"@site/versioned_docs/version-1.0.1/hello.md\\", \\"source\\": \\"@site/versioned_docs/version-1.0.1/hello.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/\\", \\"slug\\": \\"/\\",
\\"permalink\\": \\"/docs/\\", \\"permalink\\": \\"/docs/\\",
\\"version\\": \\"1.0.1\\", \\"version\\": \\"1.0.1\\",
@ -1125,6 +1280,7 @@ Object {
\\"title\\": \\"rootAbsoluteSlug\\", \\"title\\": \\"rootAbsoluteSlug\\",
\\"description\\": \\"Lorem\\", \\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/versioned_docs/version-withSlugs/rootAbsoluteSlug.md\\", \\"source\\": \\"@site/versioned_docs/version-withSlugs/rootAbsoluteSlug.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootAbsoluteSlug\\", \\"slug\\": \\"/rootAbsoluteSlug\\",
\\"permalink\\": \\"/docs/withSlugs/rootAbsoluteSlug\\", \\"permalink\\": \\"/docs/withSlugs/rootAbsoluteSlug\\",
\\"version\\": \\"withSlugs\\", \\"version\\": \\"withSlugs\\",
@ -1140,6 +1296,7 @@ Object {
\\"title\\": \\"rootRelativeSlug\\", \\"title\\": \\"rootRelativeSlug\\",
\\"description\\": \\"Lorem\\", \\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/versioned_docs/version-withSlugs/rootRelativeSlug.md\\", \\"source\\": \\"@site/versioned_docs/version-withSlugs/rootRelativeSlug.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootRelativeSlug\\", \\"slug\\": \\"/rootRelativeSlug\\",
\\"permalink\\": \\"/docs/withSlugs/rootRelativeSlug\\", \\"permalink\\": \\"/docs/withSlugs/rootRelativeSlug\\",
\\"version\\": \\"withSlugs\\", \\"version\\": \\"withSlugs\\",
@ -1154,6 +1311,7 @@ Object {
\\"title\\": \\"rootResolvedSlug\\", \\"title\\": \\"rootResolvedSlug\\",
\\"description\\": \\"Lorem\\", \\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/versioned_docs/version-withSlugs/rootResolvedSlug.md\\", \\"source\\": \\"@site/versioned_docs/version-withSlugs/rootResolvedSlug.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/hey/rootResolvedSlug\\", \\"slug\\": \\"/hey/rootResolvedSlug\\",
\\"permalink\\": \\"/docs/withSlugs/hey/rootResolvedSlug\\", \\"permalink\\": \\"/docs/withSlugs/hey/rootResolvedSlug\\",
\\"version\\": \\"withSlugs\\", \\"version\\": \\"withSlugs\\",
@ -1168,6 +1326,7 @@ Object {
\\"title\\": \\"rootTryToEscapeSlug\\", \\"title\\": \\"rootTryToEscapeSlug\\",
\\"description\\": \\"Lorem\\", \\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/versioned_docs/version-withSlugs/rootTryToEscapeSlug.md\\", \\"source\\": \\"@site/versioned_docs/version-withSlugs/rootTryToEscapeSlug.md\\",
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootTryToEscapeSlug\\", \\"slug\\": \\"/rootTryToEscapeSlug\\",
\\"permalink\\": \\"/docs/withSlugs/rootTryToEscapeSlug\\", \\"permalink\\": \\"/docs/withSlugs/rootTryToEscapeSlug\\",
\\"version\\": \\"withSlugs\\", \\"version\\": \\"withSlugs\\",
@ -1182,6 +1341,7 @@ Object {
\\"title\\": \\"absoluteSlug\\", \\"title\\": \\"absoluteSlug\\",
\\"description\\": \\"Lorem\\", \\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/versioned_docs/version-withSlugs/slugs/absoluteSlug.md\\", \\"source\\": \\"@site/versioned_docs/version-withSlugs/slugs/absoluteSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/absoluteSlug\\", \\"slug\\": \\"/absoluteSlug\\",
\\"permalink\\": \\"/docs/withSlugs/absoluteSlug\\", \\"permalink\\": \\"/docs/withSlugs/absoluteSlug\\",
\\"version\\": \\"withSlugs\\", \\"version\\": \\"withSlugs\\",
@ -1196,6 +1356,7 @@ Object {
\\"title\\": \\"relativeSlug\\", \\"title\\": \\"relativeSlug\\",
\\"description\\": \\"Lorem\\", \\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/versioned_docs/version-withSlugs/slugs/relativeSlug.md\\", \\"source\\": \\"@site/versioned_docs/version-withSlugs/slugs/relativeSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/relativeSlug\\", \\"slug\\": \\"/slugs/relativeSlug\\",
\\"permalink\\": \\"/docs/withSlugs/slugs/relativeSlug\\", \\"permalink\\": \\"/docs/withSlugs/slugs/relativeSlug\\",
\\"version\\": \\"withSlugs\\", \\"version\\": \\"withSlugs\\",
@ -1210,6 +1371,7 @@ Object {
\\"title\\": \\"resolvedSlug\\", \\"title\\": \\"resolvedSlug\\",
\\"description\\": \\"Lorem\\", \\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/versioned_docs/version-withSlugs/slugs/resolvedSlug.md\\", \\"source\\": \\"@site/versioned_docs/version-withSlugs/slugs/resolvedSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/hey/resolvedSlug\\", \\"slug\\": \\"/slugs/hey/resolvedSlug\\",
\\"permalink\\": \\"/docs/withSlugs/slugs/hey/resolvedSlug\\", \\"permalink\\": \\"/docs/withSlugs/slugs/hey/resolvedSlug\\",
\\"version\\": \\"withSlugs\\", \\"version\\": \\"withSlugs\\",
@ -1224,6 +1386,7 @@ Object {
\\"title\\": \\"tryToEscapeSlug\\", \\"title\\": \\"tryToEscapeSlug\\",
\\"description\\": \\"Lorem\\", \\"description\\": \\"Lorem\\",
\\"source\\": \\"@site/versioned_docs/version-withSlugs/slugs/tryToEscapeSlug.md\\", \\"source\\": \\"@site/versioned_docs/version-withSlugs/slugs/tryToEscapeSlug.md\\",
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/tryToEscapeSlug\\", \\"slug\\": \\"/tryToEscapeSlug\\",
\\"permalink\\": \\"/docs/withSlugs/tryToEscapeSlug\\", \\"permalink\\": \\"/docs/withSlugs/tryToEscapeSlug\\",
\\"version\\": \\"withSlugs\\", \\"version\\": \\"withSlugs\\",

View file

@ -177,6 +177,7 @@ describe('simple site', () => {
version: 'current', version: 'current',
id: 'foo/bar', id: 'foo/bar',
unversionedId: 'foo/bar', unversionedId: 'foo/bar',
sourceDirName: 'foo',
isDocsHomePage: false, isDocsHomePage: false,
permalink: '/docs/foo/bar', permalink: '/docs/foo/bar',
slug: '/foo/bar', slug: '/foo/bar',
@ -192,6 +193,7 @@ describe('simple site', () => {
version: 'current', version: 'current',
id: 'hello', id: 'hello',
unversionedId: 'hello', unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: false, isDocsHomePage: false,
permalink: '/docs/hello', permalink: '/docs/hello',
slug: '/hello', slug: '/hello',
@ -220,6 +222,7 @@ describe('simple site', () => {
version: 'current', version: 'current',
id: 'hello', id: 'hello',
unversionedId: 'hello', unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: true, isDocsHomePage: true,
permalink: '/docs/', permalink: '/docs/',
slug: '/', slug: '/',
@ -248,6 +251,7 @@ describe('simple site', () => {
version: 'current', version: 'current',
id: 'foo/bar', id: 'foo/bar',
unversionedId: 'foo/bar', unversionedId: 'foo/bar',
sourceDirName: 'foo',
isDocsHomePage: true, isDocsHomePage: true,
permalink: '/docs/', permalink: '/docs/',
slug: '/', slug: '/',
@ -279,6 +283,7 @@ describe('simple site', () => {
version: 'current', version: 'current',
id: 'foo/baz', id: 'foo/baz',
unversionedId: 'foo/baz', unversionedId: 'foo/baz',
sourceDirName: 'foo',
isDocsHomePage: false, isDocsHomePage: false,
permalink: '/docs/foo/bazSlug.html', permalink: '/docs/foo/bazSlug.html',
slug: '/foo/bazSlug.html', slug: '/foo/bazSlug.html',
@ -301,6 +306,7 @@ describe('simple site', () => {
version: 'current', version: 'current',
id: 'lorem', id: 'lorem',
unversionedId: 'lorem', unversionedId: 'lorem',
sourceDirName: '.',
isDocsHomePage: false, isDocsHomePage: false,
permalink: '/docs/lorem', permalink: '/docs/lorem',
slug: '/lorem', slug: '/lorem',
@ -336,6 +342,7 @@ describe('simple site', () => {
version: 'current', version: 'current',
id: 'foo/baz', id: 'foo/baz',
unversionedId: 'foo/baz', unversionedId: 'foo/baz',
sourceDirName: 'foo',
isDocsHomePage: false, isDocsHomePage: false,
permalink: '/docs/foo/bazSlug.html', permalink: '/docs/foo/bazSlug.html',
slug: '/foo/bazSlug.html', slug: '/foo/bazSlug.html',
@ -378,6 +385,7 @@ describe('simple site', () => {
version: 'current', version: 'current',
id: 'lorem', id: 'lorem',
unversionedId: 'lorem', unversionedId: 'lorem',
sourceDirName: '.',
isDocsHomePage: false, isDocsHomePage: false,
permalink: '/docs/lorem', permalink: '/docs/lorem',
slug: '/lorem', slug: '/lorem',
@ -549,6 +557,7 @@ describe('versioned site', () => {
await currentVersionTestUtils.testMeta(path.join('foo', 'bar.md'), { await currentVersionTestUtils.testMeta(path.join('foo', 'bar.md'), {
id: 'foo/bar', id: 'foo/bar',
unversionedId: 'foo/bar', unversionedId: 'foo/bar',
sourceDirName: 'foo',
isDocsHomePage: false, isDocsHomePage: false,
permalink: '/docs/next/foo/barSlug', permalink: '/docs/next/foo/barSlug',
slug: '/foo/barSlug', slug: '/foo/barSlug',
@ -560,6 +569,7 @@ describe('versioned site', () => {
await currentVersionTestUtils.testMeta(path.join('hello.md'), { await currentVersionTestUtils.testMeta(path.join('hello.md'), {
id: 'hello', id: 'hello',
unversionedId: 'hello', unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: false, isDocsHomePage: false,
permalink: '/docs/next/hello', permalink: '/docs/next/hello',
slug: '/hello', slug: '/hello',
@ -576,6 +586,7 @@ describe('versioned site', () => {
await version100TestUtils.testMeta(path.join('foo', 'bar.md'), { await version100TestUtils.testMeta(path.join('foo', 'bar.md'), {
id: 'version-1.0.0/foo/bar', id: 'version-1.0.0/foo/bar',
unversionedId: 'foo/bar', unversionedId: 'foo/bar',
sourceDirName: 'foo',
isDocsHomePage: false, isDocsHomePage: false,
permalink: '/docs/1.0.0/foo/barSlug', permalink: '/docs/1.0.0/foo/barSlug',
slug: '/foo/barSlug', slug: '/foo/barSlug',
@ -587,6 +598,7 @@ describe('versioned site', () => {
await version100TestUtils.testMeta(path.join('hello.md'), { await version100TestUtils.testMeta(path.join('hello.md'), {
id: 'version-1.0.0/hello', id: 'version-1.0.0/hello',
unversionedId: 'hello', unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: false, isDocsHomePage: false,
permalink: '/docs/1.0.0/hello', permalink: '/docs/1.0.0/hello',
slug: '/hello', slug: '/hello',
@ -600,6 +612,7 @@ describe('versioned site', () => {
await version101TestUtils.testMeta(path.join('foo', 'bar.md'), { await version101TestUtils.testMeta(path.join('foo', 'bar.md'), {
id: 'version-1.0.1/foo/bar', id: 'version-1.0.1/foo/bar',
unversionedId: 'foo/bar', unversionedId: 'foo/bar',
sourceDirName: 'foo',
isDocsHomePage: false, isDocsHomePage: false,
permalink: '/docs/foo/bar', permalink: '/docs/foo/bar',
slug: '/foo/bar', slug: '/foo/bar',
@ -611,6 +624,7 @@ describe('versioned site', () => {
await version101TestUtils.testMeta(path.join('hello.md'), { await version101TestUtils.testMeta(path.join('hello.md'), {
id: 'version-1.0.1/hello', id: 'version-1.0.1/hello',
unversionedId: 'hello', unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: false, isDocsHomePage: false,
permalink: '/docs/hello', permalink: '/docs/hello',
slug: '/hello', slug: '/hello',
@ -701,6 +715,7 @@ describe('versioned site', () => {
await testUtilsLocal.testMeta(path.join('hello.md'), { await testUtilsLocal.testMeta(path.join('hello.md'), {
id: 'version-1.0.0/hello', id: 'version-1.0.0/hello',
unversionedId: 'hello', unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: false, isDocsHomePage: false,
permalink: '/docs/1.0.0/hello', permalink: '/docs/1.0.0/hello',
slug: '/hello', slug: '/hello',
@ -741,6 +756,7 @@ describe('versioned site', () => {
await testUtilsLocal.testMeta(path.join('hello.md'), { await testUtilsLocal.testMeta(path.join('hello.md'), {
id: 'version-1.0.0/hello', id: 'version-1.0.0/hello',
unversionedId: 'hello', unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: false, isDocsHomePage: false,
permalink: '/docs/1.0.0/hello', permalink: '/docs/1.0.0/hello',
slug: '/hello', slug: '/hello',
@ -773,6 +789,7 @@ describe('versioned site', () => {
await testUtilsLocal.testMeta(path.join('hello.md'), { await testUtilsLocal.testMeta(path.join('hello.md'), {
id: 'version-1.0.0/hello', id: 'version-1.0.0/hello',
unversionedId: 'hello', unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: false, isDocsHomePage: false,
permalink: '/docs/1.0.0/hello', permalink: '/docs/1.0.0/hello',
slug: '/hello', slug: '/hello',
@ -806,6 +823,7 @@ describe('versioned site', () => {
await testUtilsLocal.testMeta(path.join('hello.md'), { await testUtilsLocal.testMeta(path.join('hello.md'), {
id: 'version-1.0.0/hello', id: 'version-1.0.0/hello',
unversionedId: 'hello', unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: false, isDocsHomePage: false,
permalink: '/fr/docs/1.0.0/hello', permalink: '/fr/docs/1.0.0/hello',
slug: '/hello', slug: '/hello',
@ -840,6 +858,7 @@ describe('versioned site', () => {
await testUtilsLocal.testMeta(path.join('hello.md'), { await testUtilsLocal.testMeta(path.join('hello.md'), {
id: 'version-1.0.0/hello', id: 'version-1.0.0/hello',
unversionedId: 'hello', unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: false, isDocsHomePage: false,
permalink: '/fr/docs/1.0.0/hello', permalink: '/fr/docs/1.0.0/hello',
slug: '/hello', slug: '/hello',

View file

@ -10,7 +10,7 @@
import path from 'path'; import path from 'path';
import {isMatch} from 'picomatch'; import {isMatch} from 'picomatch';
import commander from 'commander'; import commander from 'commander';
import {kebabCase} from 'lodash'; import {kebabCase, orderBy} from 'lodash';
import fs from 'fs-extra'; import fs from 'fs-extra';
import pluginContentDocs from '../index'; import pluginContentDocs from '../index';
@ -24,7 +24,7 @@ import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
import * as cliDocs from '../cli'; import * as cliDocs from '../cli';
import {OptionsSchema} from '../options'; import {OptionsSchema} from '../options';
import {normalizePluginOptions} from '@docusaurus/utils-validation'; import {normalizePluginOptions} from '@docusaurus/utils-validation';
import {DocMetadata, LoadedVersion} from '../types'; import {DocMetadata, LoadedVersion, SidebarItemsGenerator} from '../types';
import {toSidebarsProp} from '../props'; import {toSidebarsProp} from '../props';
// @ts-expect-error: TODO typedefs missing? // @ts-expect-error: TODO typedefs missing?
@ -33,6 +33,17 @@ import {validate} from 'webpack';
function findDocById(version: LoadedVersion, unversionedId: string) { function findDocById(version: LoadedVersion, unversionedId: string) {
return version.docs.find((item) => item.unversionedId === unversionedId); return version.docs.find((item) => item.unversionedId === unversionedId);
} }
function getDocById(version: LoadedVersion, unversionedId: string) {
const doc = findDocById(version, unversionedId);
if (!doc) {
throw new Error(
`No doc found with id=${unversionedId} in version ${version.versionName}.
Available ids=\n- ${version.docs.map((d) => d.unversionedId).join('\n- ')}`,
);
}
return doc;
}
const defaultDocMetadata: Partial<DocMetadata> = { const defaultDocMetadata: Partial<DocMetadata> = {
next: undefined, next: undefined,
previous: undefined, previous: undefined,
@ -40,6 +51,7 @@ const defaultDocMetadata: Partial<DocMetadata> = {
lastUpdatedAt: undefined, lastUpdatedAt: undefined,
lastUpdatedBy: undefined, lastUpdatedBy: undefined,
sidebar_label: undefined, sidebar_label: undefined,
formattedLastUpdatedAt: undefined,
}; };
const createFakeActions = (contentDir: string) => { const createFakeActions = (contentDir: string) => {
@ -203,6 +215,7 @@ describe('simple website', () => {
"sidebars.json", "sidebars.json",
"i18n/en/docusaurus-plugin-content-docs/current/**/*.{md,mdx}", "i18n/en/docusaurus-plugin-content-docs/current/**/*.{md,mdx}",
"docs/**/*.{md,mdx}", "docs/**/*.{md,mdx}",
"docs/**/_category_.{json,yml,yaml}",
] ]
`); `);
expect(isMatch('docs/hello.md', matchPattern)).toEqual(true); expect(isMatch('docs/hello.md', matchPattern)).toEqual(true);
@ -247,6 +260,7 @@ describe('simple website', () => {
version: 'current', version: 'current',
id: 'hello', id: 'hello',
unversionedId: 'hello', unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: true, isDocsHomePage: true,
permalink: '/docs/', permalink: '/docs/',
slug: '/', slug: '/',
@ -268,11 +282,12 @@ describe('simple website', () => {
}, },
}); });
expect(findDocById(currentVersion, 'foo/bar')).toEqual({ expect(getDocById(currentVersion, 'foo/bar')).toEqual({
...defaultDocMetadata, ...defaultDocMetadata,
version: 'current', version: 'current',
id: 'foo/bar', id: 'foo/bar',
unversionedId: 'foo/bar', unversionedId: 'foo/bar',
sourceDirName: 'foo',
isDocsHomePage: false, isDocsHomePage: false,
next: { next: {
title: 'baz', title: 'baz',
@ -368,15 +383,19 @@ describe('versioned website', () => {
"sidebars.json", "sidebars.json",
"i18n/en/docusaurus-plugin-content-docs/current/**/*.{md,mdx}", "i18n/en/docusaurus-plugin-content-docs/current/**/*.{md,mdx}",
"docs/**/*.{md,mdx}", "docs/**/*.{md,mdx}",
"docs/**/_category_.{json,yml,yaml}",
"versioned_sidebars/version-1.0.1-sidebars.json", "versioned_sidebars/version-1.0.1-sidebars.json",
"i18n/en/docusaurus-plugin-content-docs/version-1.0.1/**/*.{md,mdx}", "i18n/en/docusaurus-plugin-content-docs/version-1.0.1/**/*.{md,mdx}",
"versioned_docs/version-1.0.1/**/*.{md,mdx}", "versioned_docs/version-1.0.1/**/*.{md,mdx}",
"versioned_docs/version-1.0.1/**/_category_.{json,yml,yaml}",
"versioned_sidebars/version-1.0.0-sidebars.json", "versioned_sidebars/version-1.0.0-sidebars.json",
"i18n/en/docusaurus-plugin-content-docs/version-1.0.0/**/*.{md,mdx}", "i18n/en/docusaurus-plugin-content-docs/version-1.0.0/**/*.{md,mdx}",
"versioned_docs/version-1.0.0/**/*.{md,mdx}", "versioned_docs/version-1.0.0/**/*.{md,mdx}",
"versioned_docs/version-1.0.0/**/_category_.{json,yml,yaml}",
"versioned_sidebars/version-withSlugs-sidebars.json", "versioned_sidebars/version-withSlugs-sidebars.json",
"i18n/en/docusaurus-plugin-content-docs/version-withSlugs/**/*.{md,mdx}", "i18n/en/docusaurus-plugin-content-docs/version-withSlugs/**/*.{md,mdx}",
"versioned_docs/version-withSlugs/**/*.{md,mdx}", "versioned_docs/version-withSlugs/**/*.{md,mdx}",
"versioned_docs/version-withSlugs/**/_category_.{json,yml,yaml}",
] ]
`); `);
expect(isMatch('docs/hello.md', matchPattern)).toEqual(true); expect(isMatch('docs/hello.md', matchPattern)).toEqual(true);
@ -427,10 +446,11 @@ describe('versioned website', () => {
expect(findDocById(version101, 'foo/baz')).toBeUndefined(); expect(findDocById(version101, 'foo/baz')).toBeUndefined();
expect(findDocById(versionWithSlugs, 'foo/baz')).toBeUndefined(); expect(findDocById(versionWithSlugs, 'foo/baz')).toBeUndefined();
expect(findDocById(currentVersion, 'foo/bar')).toEqual({ expect(getDocById(currentVersion, 'foo/bar')).toEqual({
...defaultDocMetadata, ...defaultDocMetadata,
id: 'foo/bar', id: 'foo/bar',
unversionedId: 'foo/bar', unversionedId: 'foo/bar',
sourceDirName: 'foo',
isDocsHomePage: false, isDocsHomePage: false,
permalink: '/docs/next/foo/barSlug', permalink: '/docs/next/foo/barSlug',
slug: '/foo/barSlug', slug: '/foo/barSlug',
@ -452,10 +472,11 @@ describe('versioned website', () => {
permalink: '/docs/next/', permalink: '/docs/next/',
}, },
}); });
expect(findDocById(currentVersion, 'hello')).toEqual({ expect(getDocById(currentVersion, 'hello')).toEqual({
...defaultDocMetadata, ...defaultDocMetadata,
id: 'hello', id: 'hello',
unversionedId: 'hello', unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: true, isDocsHomePage: true,
permalink: '/docs/next/', permalink: '/docs/next/',
slug: '/', slug: '/',
@ -474,10 +495,11 @@ describe('versioned website', () => {
permalink: '/docs/next/foo/barSlug', permalink: '/docs/next/foo/barSlug',
}, },
}); });
expect(findDocById(version101, 'hello')).toEqual({ expect(getDocById(version101, 'hello')).toEqual({
...defaultDocMetadata, ...defaultDocMetadata,
id: 'version-1.0.1/hello', id: 'version-1.0.1/hello',
unversionedId: 'hello', unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: true, isDocsHomePage: true,
permalink: '/docs/', permalink: '/docs/',
slug: '/', slug: '/',
@ -496,10 +518,11 @@ describe('versioned website', () => {
permalink: '/docs/foo/bar', permalink: '/docs/foo/bar',
}, },
}); });
expect(findDocById(version100, 'foo/baz')).toEqual({ expect(getDocById(version100, 'foo/baz')).toEqual({
...defaultDocMetadata, ...defaultDocMetadata,
id: 'version-1.0.0/foo/baz', id: 'version-1.0.0/foo/baz',
unversionedId: 'foo/baz', unversionedId: 'foo/baz',
sourceDirName: 'foo',
isDocsHomePage: false, isDocsHomePage: false,
permalink: '/docs/1.0.0/foo/baz', permalink: '/docs/1.0.0/foo/baz',
slug: '/foo/baz', slug: '/foo/baz',
@ -611,9 +634,11 @@ describe('versioned website (community)', () => {
"community_sidebars.json", "community_sidebars.json",
"i18n/en/docusaurus-plugin-content-docs-community/current/**/*.{md,mdx}", "i18n/en/docusaurus-plugin-content-docs-community/current/**/*.{md,mdx}",
"community/**/*.{md,mdx}", "community/**/*.{md,mdx}",
"community/**/_category_.{json,yml,yaml}",
"community_versioned_sidebars/version-1.0.0-sidebars.json", "community_versioned_sidebars/version-1.0.0-sidebars.json",
"i18n/en/docusaurus-plugin-content-docs-community/version-1.0.0/**/*.{md,mdx}", "i18n/en/docusaurus-plugin-content-docs-community/version-1.0.0/**/*.{md,mdx}",
"community_versioned_docs/version-1.0.0/**/*.{md,mdx}", "community_versioned_docs/version-1.0.0/**/*.{md,mdx}",
"community_versioned_docs/version-1.0.0/**/_category_.{json,yml,yaml}",
] ]
`); `);
expect(isMatch('community/team.md', matchPattern)).toEqual(true); expect(isMatch('community/team.md', matchPattern)).toEqual(true);
@ -644,10 +669,11 @@ describe('versioned website (community)', () => {
expect(content.loadedVersions.length).toEqual(2); expect(content.loadedVersions.length).toEqual(2);
const [currentVersion, version100] = content.loadedVersions; const [currentVersion, version100] = content.loadedVersions;
expect(findDocById(currentVersion, 'team')).toEqual({ expect(getDocById(currentVersion, 'team')).toEqual({
...defaultDocMetadata, ...defaultDocMetadata,
id: 'team', id: 'team',
unversionedId: 'team', unversionedId: 'team',
sourceDirName: '.',
isDocsHomePage: false, isDocsHomePage: false,
permalink: '/community/next/team', permalink: '/community/next/team',
slug: '/team', slug: '/team',
@ -659,10 +685,11 @@ describe('versioned website (community)', () => {
sidebar: 'community', sidebar: 'community',
frontMatter: {title: 'Team title translated'}, frontMatter: {title: 'Team title translated'},
}); });
expect(findDocById(version100, 'team')).toEqual({ expect(getDocById(version100, 'team')).toEqual({
...defaultDocMetadata, ...defaultDocMetadata,
id: 'version-1.0.0/team', id: 'version-1.0.0/team',
unversionedId: 'team', unversionedId: 'team',
sourceDirName: '.',
isDocsHomePage: false, isDocsHomePage: false,
permalink: '/community/team', permalink: '/community/team',
slug: '/team', slug: '/team',
@ -709,7 +736,7 @@ describe('site with doc label', () => {
}), }),
); );
const content = await plugin.loadContent(); const content = (await plugin.loadContent?.())!;
return {content}; return {content};
} }
@ -730,3 +757,807 @@ describe('site with doc label', () => {
expect(sidebarProps.docs[1].label).toBe('Hello 2 From Doc'); expect(sidebarProps.docs[1].label).toBe('Hello 2 From Doc');
}); });
}); });
describe('site with full autogenerated sidebar', () => {
async function loadSite() {
const siteDir = path.join(
__dirname,
'__fixtures__',
'site-with-autogenerated-sidebar',
);
const context = await loadContext(siteDir);
const plugin = pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
path: 'docs',
}),
);
const content = (await plugin.loadContent?.())!;
return {content, siteDir};
}
test('sidebar is fully autogenerated', async () => {
const {content} = await loadSite();
const version = content.loadedVersions[0];
expect(version.sidebars).toEqual({
defaultSidebar: [
{
type: 'doc',
id: 'getting-started',
},
{
type: 'doc',
id: 'installation',
},
{
type: 'category',
label: 'Guides',
collapsed: true,
items: [
{
type: 'doc',
id: 'Guides/guide1',
},
{
type: 'doc',
id: 'Guides/guide2',
},
{
type: 'doc',
id: 'Guides/guide2.5',
},
{
type: 'doc',
id: 'Guides/guide3',
},
{
type: 'doc',
id: 'Guides/guide4',
},
{
type: 'doc',
id: 'Guides/guide5',
},
],
},
{
type: 'category',
label: 'API (label from _category_.json)',
collapsed: true,
items: [
{
type: 'doc',
id: 'API/api-overview',
},
{
type: 'category',
label: 'Core APIs',
collapsed: true,
items: [
{
type: 'doc',
id: 'API/Core APIs/Client API',
},
{
type: 'doc',
id: 'API/Core APIs/Server API',
},
],
},
{
type: 'category',
label: 'Extension APIs (label from _category_.yml)',
collapsed: true,
items: [
{
type: 'doc',
id: 'API/Extension APIs/Plugin API',
},
{
type: 'doc',
id: 'API/Extension APIs/Theme API',
},
],
},
{
type: 'doc',
id: 'API/api-end',
},
],
},
],
});
});
test('docs in fully generated sidebar have correct metadatas', async () => {
const {content, siteDir} = await loadSite();
const version = content.loadedVersions[0];
expect(getDocById(version, 'getting-started')).toEqual({
...defaultDocMetadata,
id: 'getting-started',
unversionedId: 'getting-started',
sourceDirName: '.',
isDocsHomePage: false,
permalink: '/docs/getting-started',
slug: '/getting-started',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'0-getting-started.md',
),
title: 'Getting Started',
description: 'Getting started text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {},
sidebarPosition: 0,
previous: undefined,
next: {
permalink: '/docs/installation',
title: 'Installation',
},
});
expect(getDocById(version, 'installation')).toEqual({
...defaultDocMetadata,
id: 'installation',
unversionedId: 'installation',
sourceDirName: '.',
isDocsHomePage: false,
permalink: '/docs/installation',
slug: '/installation',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'1-installation.md',
),
title: 'Installation',
description: 'Installation text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {},
sidebarPosition: 1,
previous: {
permalink: '/docs/getting-started',
title: 'Getting Started',
},
next: {
permalink: '/docs/Guides/guide1',
title: 'Guide 1',
},
});
expect(getDocById(version, 'Guides/guide1')).toEqual({
...defaultDocMetadata,
id: 'Guides/guide1',
unversionedId: 'Guides/guide1',
sourceDirName: 'Guides',
isDocsHomePage: false,
permalink: '/docs/Guides/guide1',
slug: '/Guides/guide1',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'Guides',
'z-guide1.md',
),
title: 'Guide 1',
description: 'Guide 1 text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {
id: 'guide1',
sidebar_position: 1,
},
sidebarPosition: 1,
previous: {
permalink: '/docs/installation',
title: 'Installation',
},
next: {
permalink: '/docs/Guides/guide2',
title: 'Guide 2',
},
});
expect(getDocById(version, 'Guides/guide2')).toEqual({
...defaultDocMetadata,
id: 'Guides/guide2',
unversionedId: 'Guides/guide2',
sourceDirName: 'Guides',
isDocsHomePage: false,
permalink: '/docs/Guides/guide2',
slug: '/Guides/guide2',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'Guides',
'02-guide2.md',
),
title: 'Guide 2',
description: 'Guide 2 text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {
id: 'guide2',
},
sidebarPosition: 2,
previous: {
permalink: '/docs/Guides/guide1',
title: 'Guide 1',
},
next: {
permalink: '/docs/Guides/guide2.5',
title: 'Guide 2.5',
},
});
expect(getDocById(version, 'Guides/guide2.5')).toEqual({
...defaultDocMetadata,
id: 'Guides/guide2.5',
unversionedId: 'Guides/guide2.5',
sourceDirName: 'Guides',
isDocsHomePage: false,
permalink: '/docs/Guides/guide2.5',
slug: '/Guides/guide2.5',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'Guides',
'0-guide2.5.md',
),
title: 'Guide 2.5',
description: 'Guide 2.5 text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {
id: 'guide2.5',
sidebar_position: 2.5,
},
sidebarPosition: 2.5,
previous: {
permalink: '/docs/Guides/guide2',
title: 'Guide 2',
},
next: {
permalink: '/docs/Guides/guide3',
title: 'Guide 3',
},
});
expect(getDocById(version, 'Guides/guide3')).toEqual({
...defaultDocMetadata,
id: 'Guides/guide3',
unversionedId: 'Guides/guide3',
sourceDirName: 'Guides',
isDocsHomePage: false,
permalink: '/docs/Guides/guide3',
slug: '/Guides/guide3',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'Guides',
'guide3.md',
),
title: 'Guide 3',
description: 'Guide 3 text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {
id: 'guide3',
sidebar_position: 3,
},
sidebarPosition: 3,
previous: {
permalink: '/docs/Guides/guide2.5',
title: 'Guide 2.5',
},
next: {
permalink: '/docs/Guides/guide4',
title: 'Guide 4',
},
});
expect(getDocById(version, 'Guides/guide4')).toEqual({
...defaultDocMetadata,
id: 'Guides/guide4',
unversionedId: 'Guides/guide4',
sourceDirName: 'Guides',
isDocsHomePage: false,
permalink: '/docs/Guides/guide4',
slug: '/Guides/guide4',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'Guides',
'a-guide4.md',
),
title: 'Guide 4',
description: 'Guide 4 text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {
id: 'guide4',
},
sidebarPosition: undefined,
previous: {
permalink: '/docs/Guides/guide3',
title: 'Guide 3',
},
next: {
permalink: '/docs/Guides/guide5',
title: 'Guide 5',
},
});
expect(getDocById(version, 'Guides/guide5')).toEqual({
...defaultDocMetadata,
id: 'Guides/guide5',
unversionedId: 'Guides/guide5',
sourceDirName: 'Guides',
isDocsHomePage: false,
permalink: '/docs/Guides/guide5',
slug: '/Guides/guide5',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'Guides',
'b-guide5.md',
),
title: 'Guide 5',
description: 'Guide 5 text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {
id: 'guide5',
},
sidebarPosition: undefined,
previous: {
permalink: '/docs/Guides/guide4',
title: 'Guide 4',
},
next: {
permalink: '/docs/API/api-overview',
title: 'API Overview',
},
});
expect(getDocById(version, 'API/api-overview')).toEqual({
...defaultDocMetadata,
id: 'API/api-overview',
unversionedId: 'API/api-overview',
sourceDirName: '3-API',
isDocsHomePage: false,
permalink: '/docs/API/api-overview',
slug: '/API/api-overview',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'3-API',
'00_api-overview.md',
),
title: 'API Overview',
description: 'API Overview text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {},
sidebarPosition: 0,
previous: {
permalink: '/docs/Guides/guide5',
title: 'Guide 5',
},
next: {
permalink: '/docs/API/Core APIs/Client API',
title: 'Client API',
},
});
expect(getDocById(version, 'API/Core APIs/Client API')).toEqual({
...defaultDocMetadata,
id: 'API/Core APIs/Client API',
unversionedId: 'API/Core APIs/Client API',
sourceDirName: '3-API/01_Core APIs',
isDocsHomePage: false,
permalink: '/docs/API/Core APIs/Client API',
slug: '/API/Core APIs/Client API',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'3-API',
'01_Core APIs',
'0 --- Client API.md',
),
title: 'Client API',
description: 'Client API text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {},
sidebarPosition: 0,
previous: {
permalink: '/docs/API/api-overview',
title: 'API Overview',
},
next: {
permalink: '/docs/API/Core APIs/Server API',
title: 'Server API',
},
});
expect(getDocById(version, 'API/Core APIs/Server API')).toEqual({
...defaultDocMetadata,
id: 'API/Core APIs/Server API',
unversionedId: 'API/Core APIs/Server API',
sourceDirName: '3-API/01_Core APIs',
isDocsHomePage: false,
permalink: '/docs/API/Core APIs/Server API',
slug: '/API/Core APIs/Server API',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'3-API',
'01_Core APIs',
'1 --- Server API.md',
),
title: 'Server API',
description: 'Server API text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {},
sidebarPosition: 1,
previous: {
permalink: '/docs/API/Core APIs/Client API',
title: 'Client API',
},
next: {
permalink: '/docs/API/Extension APIs/Plugin API',
title: 'Plugin API',
},
});
expect(getDocById(version, 'API/Extension APIs/Plugin API')).toEqual({
...defaultDocMetadata,
id: 'API/Extension APIs/Plugin API',
unversionedId: 'API/Extension APIs/Plugin API',
sourceDirName: '3-API/02_Extension APIs',
isDocsHomePage: false,
permalink: '/docs/API/Extension APIs/Plugin API',
slug: '/API/Extension APIs/Plugin API',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'3-API',
'02_Extension APIs',
'0. Plugin API.md',
),
title: 'Plugin API',
description: 'Plugin API text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {},
sidebarPosition: 0,
previous: {
permalink: '/docs/API/Core APIs/Server API',
title: 'Server API',
},
next: {
permalink: '/docs/API/Extension APIs/Theme API',
title: 'Theme API',
},
});
expect(getDocById(version, 'API/Extension APIs/Theme API')).toEqual({
...defaultDocMetadata,
id: 'API/Extension APIs/Theme API',
unversionedId: 'API/Extension APIs/Theme API',
sourceDirName: '3-API/02_Extension APIs',
isDocsHomePage: false,
permalink: '/docs/API/Extension APIs/Theme API',
slug: '/API/Extension APIs/Theme API',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'3-API',
'02_Extension APIs',
'1. Theme API.md',
),
title: 'Theme API',
description: 'Theme API text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {},
sidebarPosition: 1,
previous: {
permalink: '/docs/API/Extension APIs/Plugin API',
title: 'Plugin API',
},
next: {
permalink: '/docs/API/api-end',
title: 'API End',
},
});
expect(getDocById(version, 'API/api-end')).toEqual({
...defaultDocMetadata,
id: 'API/api-end',
unversionedId: 'API/api-end',
sourceDirName: '3-API',
isDocsHomePage: false,
permalink: '/docs/API/api-end',
slug: '/API/api-end',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'3-API',
'03_api-end.md',
),
title: 'API End',
description: 'API End text',
version: 'current',
sidebar: 'defaultSidebar',
frontMatter: {},
sidebarPosition: 3,
previous: {
permalink: '/docs/API/Extension APIs/Theme API',
title: 'Theme API',
},
next: undefined,
});
});
});
describe('site with partial autogenerated sidebars', () => {
async function loadSite() {
const siteDir = path.join(
__dirname,
'__fixtures__',
'site-with-autogenerated-sidebar',
);
const context = await loadContext(siteDir, {});
const plugin = pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
path: 'docs',
sidebarPath: path.join(
__dirname,
'__fixtures__',
'site-with-autogenerated-sidebar',
'partialAutogeneratedSidebars.js',
),
}),
);
const content = (await plugin.loadContent?.())!;
return {content, siteDir};
}
test('sidebar is partially autogenerated', async () => {
const {content} = await loadSite();
const version = content.loadedVersions[0];
expect(version.sidebars).toEqual({
someSidebar: [
{
type: 'doc',
id: 'API/api-end',
},
{
type: 'category',
label: 'Some category',
collapsed: true,
items: [
{
type: 'doc',
id: 'API/api-overview',
},
{
type: 'doc',
id: 'API/Extension APIs/Plugin API',
},
{
type: 'doc',
id: 'API/Extension APIs/Theme API',
},
],
},
],
});
});
test('docs in partially generated sidebar have correct metadatas', async () => {
const {content, siteDir} = await loadSite();
const version = content.loadedVersions[0];
// Only looking at the docs of the autogen sidebar, others metadatas should not be affected
expect(getDocById(version, 'API/api-end')).toEqual({
...defaultDocMetadata,
id: 'API/api-end',
unversionedId: 'API/api-end',
sourceDirName: '3-API',
isDocsHomePage: false,
permalink: '/docs/API/api-end',
slug: '/API/api-end',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'3-API',
'03_api-end.md',
),
title: 'API End',
description: 'API End text',
version: 'current',
sidebar: 'someSidebar',
frontMatter: {},
sidebarPosition: 3, // ignored (not part of the autogenerated sidebar slice)
previous: undefined,
next: {
permalink: '/docs/API/api-overview',
title: 'API Overview',
},
});
expect(getDocById(version, 'API/api-overview')).toEqual({
...defaultDocMetadata,
id: 'API/api-overview',
unversionedId: 'API/api-overview',
sourceDirName: '3-API',
isDocsHomePage: false,
permalink: '/docs/API/api-overview',
slug: '/API/api-overview',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'3-API',
'00_api-overview.md',
),
title: 'API Overview',
description: 'API Overview text',
version: 'current',
sidebar: 'someSidebar',
frontMatter: {},
sidebarPosition: 0, // ignored (not part of the autogenerated sidebar slice)
previous: {
permalink: '/docs/API/api-end',
title: 'API End',
},
next: {
permalink: '/docs/API/Extension APIs/Plugin API',
title: 'Plugin API',
},
});
expect(getDocById(version, 'API/Extension APIs/Plugin API')).toEqual({
...defaultDocMetadata,
id: 'API/Extension APIs/Plugin API',
unversionedId: 'API/Extension APIs/Plugin API',
sourceDirName: '3-API/02_Extension APIs',
isDocsHomePage: false,
permalink: '/docs/API/Extension APIs/Plugin API',
slug: '/API/Extension APIs/Plugin API',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'3-API',
'02_Extension APIs',
'0. Plugin API.md',
),
title: 'Plugin API',
description: 'Plugin API text',
version: 'current',
sidebar: 'someSidebar',
frontMatter: {},
sidebarPosition: 0,
previous: {
permalink: '/docs/API/api-overview',
title: 'API Overview',
},
next: {
permalink: '/docs/API/Extension APIs/Theme API',
title: 'Theme API',
},
});
expect(getDocById(version, 'API/Extension APIs/Theme API')).toEqual({
...defaultDocMetadata,
id: 'API/Extension APIs/Theme API',
unversionedId: 'API/Extension APIs/Theme API',
sourceDirName: '3-API/02_Extension APIs',
isDocsHomePage: false,
permalink: '/docs/API/Extension APIs/Theme API',
slug: '/API/Extension APIs/Theme API',
source: path.posix.join(
'@site',
posixPath(path.relative(siteDir, version.contentPath)),
'3-API',
'02_Extension APIs',
'1. Theme API.md',
),
title: 'Theme API',
description: 'Theme API text',
version: 'current',
sidebar: 'someSidebar',
frontMatter: {},
sidebarPosition: 1,
previous: {
permalink: '/docs/API/Extension APIs/Plugin API',
title: 'Plugin API',
},
next: undefined,
});
});
});
describe('site with custom sidebar items generator', () => {
async function loadSite(sidebarItemsGenerator: SidebarItemsGenerator) {
const siteDir = path.join(
__dirname,
'__fixtures__',
'site-with-autogenerated-sidebar',
);
const context = await loadContext(siteDir);
const plugin = pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
path: 'docs',
sidebarItemsGenerator,
}),
);
const content = (await plugin.loadContent?.())!;
return {content, siteDir};
}
test('sidebar is autogenerated according to custom sidebarItemsGenerator', async () => {
const customSidebarItemsGenerator: SidebarItemsGenerator = async () => {
return [
{type: 'doc', id: 'API/api-overview'},
{type: 'doc', id: 'API/api-end'},
];
};
const customSidebarItemsGeneratorMock: SidebarItemsGenerator = jest.fn(
customSidebarItemsGenerator,
);
const {content} = await loadSite(customSidebarItemsGeneratorMock);
const version = content.loadedVersions[0];
expect(version.sidebars).toEqual({
defaultSidebar: [
{type: 'doc', id: 'API/api-overview'},
{type: 'doc', id: 'API/api-end'},
],
});
});
test('sidebarItemsGenerator is called with appropriate data', async () => {
type GeneratorArg = Parameters<SidebarItemsGenerator>[0];
const customSidebarItemsGeneratorMock = jest.fn(
async (_arg: GeneratorArg) => [],
);
const {siteDir} = await loadSite(customSidebarItemsGeneratorMock);
const generatorArg: GeneratorArg =
customSidebarItemsGeneratorMock.mock.calls[0][0];
// Make test pass even if docs are in different order and paths are absolutes
function makeDeterministic(arg: GeneratorArg): GeneratorArg {
return {
...arg,
docs: orderBy(arg.docs, 'id'),
version: {
...arg.version,
contentPath: path.relative(siteDir, arg.version.contentPath),
},
};
}
expect(makeDeterministic(generatorArg)).toMatchSnapshot();
});
});

View file

@ -0,0 +1,115 @@
/**
* 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 {
extractNumberPrefix,
stripNumberPrefix,
stripPathNumberPrefixes,
} from '../numberPrefix';
const BadNumberPrefixPatterns = [
'a1-My Doc',
'My Doc-000',
'00abc01-My Doc',
'My 001- Doc',
'My -001 Doc',
];
describe('stripNumberPrefix', () => {
test('should strip number prefix if present', () => {
expect(stripNumberPrefix('1-My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('01-My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001-My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001 - My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001 - My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('999 - My Doc')).toEqual('My Doc');
//
expect(stripNumberPrefix('1---My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('01---My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001---My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001 --- My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001 --- My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('999 --- My Doc')).toEqual('My Doc');
//
expect(stripNumberPrefix('1___My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('01___My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001___My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001 ___ My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001 ___ My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('999 ___ My Doc')).toEqual('My Doc');
//
expect(stripNumberPrefix('1.My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('01.My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001.My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001 . My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('001 . My Doc')).toEqual('My Doc');
expect(stripNumberPrefix('999 . My Doc')).toEqual('My Doc');
});
test('should not strip number prefix if pattern does not match', () => {
BadNumberPrefixPatterns.forEach((badPattern) => {
expect(stripNumberPrefix(badPattern)).toEqual(badPattern);
});
});
});
describe('stripPathNumberPrefix', () => {
test('should strip number prefixes in paths', () => {
expect(
stripPathNumberPrefixes(
'0-MyRootFolder0/1 - MySubFolder1/2. MyDeepFolder2/3 _MyDoc3',
),
).toEqual('MyRootFolder0/MySubFolder1/MyDeepFolder2/MyDoc3');
});
});
describe('extractNumberPrefix', () => {
test('should extract number prefix if present', () => {
expect(extractNumberPrefix('0-My Doc')).toEqual({
filename: 'My Doc',
numberPrefix: 0,
});
expect(extractNumberPrefix('1-My Doc')).toEqual({
filename: 'My Doc',
numberPrefix: 1,
});
expect(extractNumberPrefix('01-My Doc')).toEqual({
filename: 'My Doc',
numberPrefix: 1,
});
expect(extractNumberPrefix('001-My Doc')).toEqual({
filename: 'My Doc',
numberPrefix: 1,
});
expect(extractNumberPrefix('001 - My Doc')).toEqual({
filename: 'My Doc',
numberPrefix: 1,
});
expect(extractNumberPrefix('001 - My Doc')).toEqual({
filename: 'My Doc',
numberPrefix: 1,
});
expect(extractNumberPrefix('999 - My Doc')).toEqual({
filename: 'My Doc',
numberPrefix: 999,
});
expect(extractNumberPrefix('0046036 - My Doc')).toEqual({
filename: 'My Doc',
numberPrefix: 46036,
});
});
test('should not extract number prefix if pattern does not match', () => {
BadNumberPrefixPatterns.forEach((badPattern) => {
expect(extractNumberPrefix(badPattern)).toEqual({
filename: badPattern,
numberPrefix: undefined,
});
});
});
});

View file

@ -7,6 +7,7 @@
import {OptionsSchema, DEFAULT_OPTIONS} from '../options'; import {OptionsSchema, DEFAULT_OPTIONS} from '../options';
import {normalizePluginOptions} from '@docusaurus/utils-validation'; import {normalizePluginOptions} from '@docusaurus/utils-validation';
import {DefaultSidebarItemsGenerator} from '../sidebarItemsGenerator';
// the type of remark/rehype plugins is function // the type of remark/rehype plugins is function
const markdownPluginsFunctionStub = () => {}; const markdownPluginsFunctionStub = () => {};
@ -26,6 +27,7 @@ describe('normalizeDocsPluginOptions', () => {
homePageId: 'home', // Document id for docs home page. homePageId: 'home', // Document id for docs home page.
include: ['**/*.{md,mdx}'], // Extensions to include. include: ['**/*.{md,mdx}'], // Extensions to include.
sidebarPath: 'my-sidebar', // Path to sidebar configuration for showing a list of markdown pages. sidebarPath: 'my-sidebar', // Path to sidebar configuration for showing a list of markdown pages.
sidebarItemsGenerator: DefaultSidebarItemsGenerator,
docLayoutComponent: '@theme/DocPage', docLayoutComponent: '@theme/DocPage',
docItemComponent: '@theme/DocItem', docItemComponent: '@theme/DocItem',
remarkPlugins: [markdownPluginsObjectStub], remarkPlugins: [markdownPluginsObjectStub],

View file

@ -0,0 +1,268 @@
/**
* 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 {
CategoryMetadatasFile,
DefaultSidebarItemsGenerator,
} from '../sidebarItemsGenerator';
import {DefaultCategoryCollapsedValue} from '../sidebars';
import {Sidebar, SidebarItemsGenerator} from '../types';
import fs from 'fs-extra';
describe('DefaultSidebarItemsGenerator', () => {
function testDefaultSidebarItemsGenerator(
options: Partial<Parameters<SidebarItemsGenerator>[0]>,
) {
return DefaultSidebarItemsGenerator({
item: {
type: 'autogenerated',
dirName: '.',
},
version: {
versionName: 'current',
contentPath: 'docs',
},
docs: [],
...options,
});
}
function mockCategoryMetadataFiles(
categoryMetadataFiles: Record<string, Partial<CategoryMetadatasFile>>,
) {
jest.spyOn(fs, 'pathExists').mockImplementation((metadataFilePath) => {
return typeof categoryMetadataFiles[metadataFilePath] !== 'undefined';
});
jest.spyOn(fs, 'readFile').mockImplementation(
// @ts-expect-error: annoying TS error due to overrides
async (metadataFilePath: string) => {
return JSON.stringify(categoryMetadataFiles[metadataFilePath]);
},
);
}
test('generates empty sidebar slice when no docs and emit a warning', async () => {
const consoleWarn = jest.spyOn(console, 'warn');
const sidebarSlice = await testDefaultSidebarItemsGenerator({
docs: [],
});
expect(sidebarSlice).toEqual([]);
expect(consoleWarn).toHaveBeenCalledWith(
expect.stringMatching(
/No docs found in dir .: can't auto-generate a sidebar/,
),
);
});
test('generates simple flat sidebar', async () => {
const sidebarSlice = await DefaultSidebarItemsGenerator({
item: {
type: 'autogenerated',
dirName: '.',
},
version: {
versionName: 'current',
contentPath: '',
},
docs: [
{
id: 'doc1',
source: 'doc1.md',
sourceDirName: '.',
sidebarPosition: 2,
frontMatter: {
sidebar_label: 'doc1 sidebar label',
},
},
{
id: 'doc2',
source: 'doc2.md',
sourceDirName: '.',
sidebarPosition: 3,
frontMatter: {},
},
{
id: 'doc3',
source: 'doc3.md',
sourceDirName: '.',
sidebarPosition: 1,
frontMatter: {},
},
{
id: 'doc4',
source: 'doc4.md',
sourceDirName: '.',
sidebarPosition: 1.5,
frontMatter: {},
},
{
id: 'doc5',
source: 'doc5.md',
sourceDirName: '.',
sidebarPosition: undefined,
frontMatter: {},
},
],
});
expect(sidebarSlice).toEqual([
{type: 'doc', id: 'doc3'},
{type: 'doc', id: 'doc4'},
{type: 'doc', id: 'doc1', label: 'doc1 sidebar label'},
{type: 'doc', id: 'doc2'},
{type: 'doc', id: 'doc5'},
] as Sidebar);
});
test('generates complex nested sidebar', async () => {
mockCategoryMetadataFiles({
'02-Guides/_category_.json': {collapsed: false},
'02-Guides/01-SubGuides/_category_.yml': {
label: 'SubGuides (metadata file label)',
},
});
const sidebarSlice = await DefaultSidebarItemsGenerator({
item: {
type: 'autogenerated',
dirName: '.',
},
version: {
versionName: 'current',
contentPath: '',
},
docs: [
{
id: 'intro',
source: 'intro.md',
sourceDirName: '.',
sidebarPosition: 1,
frontMatter: {},
},
{
id: 'tutorial2',
source: 'tutorial2.md',
sourceDirName: '01-Tutorials',
sidebarPosition: 2,
frontMatter: {},
},
{
id: 'tutorial1',
source: 'tutorial1.md',
sourceDirName: '01-Tutorials',
sidebarPosition: 1,
frontMatter: {},
},
{
id: 'guide2',
source: 'guide2.md',
sourceDirName: '02-Guides',
sidebarPosition: 2,
frontMatter: {},
},
{
id: 'guide1',
source: 'guide1.md',
sourceDirName: '02-Guides',
sidebarPosition: 1,
frontMatter: {},
},
{
id: 'nested-guide',
source: 'nested-guide.md',
sourceDirName: '02-Guides/01-SubGuides',
sidebarPosition: undefined,
frontMatter: {},
},
{
id: 'end',
source: 'end.md',
sourceDirName: '.',
sidebarPosition: 3,
frontMatter: {},
},
],
});
expect(sidebarSlice).toEqual([
{type: 'doc', id: 'intro'},
{
type: 'category',
label: 'Tutorials',
collapsed: DefaultCategoryCollapsedValue,
items: [
{type: 'doc', id: 'tutorial1'},
{type: 'doc', id: 'tutorial2'},
],
},
{
type: 'category',
label: 'Guides',
collapsed: false,
items: [
{type: 'doc', id: 'guide1'},
{
type: 'category',
label: 'SubGuides (metadata file label)',
collapsed: DefaultCategoryCollapsedValue,
items: [{type: 'doc', id: 'nested-guide'}],
},
{type: 'doc', id: 'guide2'},
],
},
{type: 'doc', id: 'end'},
] as Sidebar);
});
test('generates subfolder sidebar', async () => {
const sidebarSlice = await DefaultSidebarItemsGenerator({
item: {
type: 'autogenerated',
dirName: 'subfolder/subsubfolder',
},
version: {
versionName: 'current',
contentPath: '',
},
docs: [
{
id: 'doc1',
source: 'doc1.md',
sourceDirName: 'subfolder/subsubfolder',
sidebarPosition: undefined,
frontMatter: {},
},
{
id: 'doc2',
source: 'doc2.md',
sourceDirName: 'subfolder',
sidebarPosition: undefined,
frontMatter: {},
},
{
id: 'doc3',
source: 'doc3.md',
sourceDirName: '.',
sidebarPosition: undefined,
frontMatter: {},
},
{
id: 'doc4',
source: 'doc4.md',
sourceDirName: 'subfolder/subsubfolder',
sidebarPosition: undefined,
frontMatter: {},
},
],
});
expect(sidebarSlice).toEqual([
{type: 'doc', id: 'doc1'},
{type: 'doc', id: 'doc4'},
] as Sidebar);
});
});

View file

@ -14,8 +14,16 @@ import {
collectSidebarCategories, collectSidebarCategories,
collectSidebarLinks, collectSidebarLinks,
transformSidebarItems, transformSidebarItems,
DefaultSidebars,
processSidebars,
} from '../sidebars'; } from '../sidebars';
import {Sidebar, Sidebars} from '../types'; import {
Sidebar,
SidebarItem,
SidebarItemsGenerator,
Sidebars,
UnprocessedSidebars,
} from '../types';
/* eslint-disable global-require, import/no-dynamic-require */ /* eslint-disable global-require, import/no-dynamic-require */
@ -124,7 +132,7 @@ describe('loadSidebars', () => {
); );
*/ */
// See https://github.com/facebook/docusaurus/issues/3366 // See https://github.com/facebook/docusaurus/issues/3366
expect(loadSidebars('badpath')).toEqual({}); expect(loadSidebars('badpath')).toEqual(DefaultSidebars);
}); });
test('undefined path', () => { test('undefined path', () => {
@ -443,6 +451,131 @@ describe('transformSidebarItems', () => {
}); });
}); });
describe('processSidebars', () => {
const StaticGeneratedSidebarSlice: SidebarItem[] = [
{type: 'doc', id: 'doc-generated-id-1'},
{type: 'doc', id: 'doc-generated-id-2'},
];
const StaticSidebarItemsGenerator: SidebarItemsGenerator = jest.fn(
async () => {
return StaticGeneratedSidebarSlice;
},
);
async function testProcessSidebars(unprocessedSidebars: UnprocessedSidebars) {
return processSidebars({
sidebarItemsGenerator: StaticSidebarItemsGenerator,
unprocessedSidebars,
docs: [],
// @ts-expect-error: useless for this test
version: {},
});
}
test('let sidebars without autogenerated items untouched', async () => {
const unprocessedSidebars: UnprocessedSidebars = {
someSidebar: [
{type: 'doc', id: 'doc1'},
{
type: 'category',
collapsed: false,
items: [{type: 'doc', id: 'doc2'}],
label: 'Category',
},
{type: 'link', href: 'https://facebook.com', label: 'FB'},
],
secondSidebar: [
{type: 'doc', id: 'doc3'},
{type: 'link', href: 'https://instagram.com', label: 'IG'},
{
type: 'category',
collapsed: false,
items: [{type: 'doc', id: 'doc4'}],
label: 'Category',
},
],
};
const processedSidebar = await testProcessSidebars(unprocessedSidebars);
expect(processedSidebar).toEqual(unprocessedSidebars);
});
test('replace autogenerated items by generated sidebars slices', async () => {
const unprocessedSidebars: UnprocessedSidebars = {
someSidebar: [
{type: 'doc', id: 'doc1'},
{
type: 'category',
collapsed: false,
items: [
{type: 'doc', id: 'doc2'},
{type: 'autogenerated', dirName: 'dir1'},
],
label: 'Category',
},
{type: 'link', href: 'https://facebook.com', label: 'FB'},
],
secondSidebar: [
{type: 'doc', id: 'doc3'},
{type: 'autogenerated', dirName: 'dir2'},
{type: 'link', href: 'https://instagram.com', label: 'IG'},
{type: 'autogenerated', dirName: 'dir3'},
{
type: 'category',
collapsed: false,
items: [{type: 'doc', id: 'doc4'}],
label: 'Category',
},
],
};
const processedSidebar = await testProcessSidebars(unprocessedSidebars);
expect(StaticSidebarItemsGenerator).toHaveBeenCalledTimes(3);
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
item: {type: 'autogenerated', dirName: 'dir1'},
docs: [],
version: {},
});
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
item: {type: 'autogenerated', dirName: 'dir2'},
docs: [],
version: {},
});
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
item: {type: 'autogenerated', dirName: 'dir3'},
docs: [],
version: {},
});
expect(processedSidebar).toEqual({
someSidebar: [
{type: 'doc', id: 'doc1'},
{
type: 'category',
collapsed: false,
items: [{type: 'doc', id: 'doc2'}, ...StaticGeneratedSidebarSlice],
label: 'Category',
},
{type: 'link', href: 'https://facebook.com', label: 'FB'},
],
secondSidebar: [
{type: 'doc', id: 'doc3'},
...StaticGeneratedSidebarSlice,
{type: 'link', href: 'https://instagram.com', label: 'IG'},
...StaticGeneratedSidebarSlice,
{
type: 'category',
collapsed: false,
items: [{type: 'doc', id: 'doc4'}],
label: 'Category',
},
],
} as Sidebars);
});
});
describe('createSidebarsUtils', () => { describe('createSidebarsUtils', () => {
const sidebar1: Sidebar = [ const sidebar1: Sidebar = [
{ {

View file

@ -15,6 +15,23 @@ describe('getSlug', () => {
); );
}); });
test('can strip dir number prefixes', () => {
expect(
getSlug({
baseID: 'doc',
dirName: '/001-dir1/002-dir2',
stripDirNumberPrefixes: true,
}),
).toEqual('/dir1/dir2/doc');
expect(
getSlug({
baseID: 'doc',
dirName: '/001-dir1/002-dir2',
stripDirNumberPrefixes: false,
}),
).toEqual('/001-dir1/002-dir2/doc');
});
// See https://github.com/facebook/docusaurus/issues/3223 // See https://github.com/facebook/docusaurus/issues/3223
test('should handle special chars in doc path', () => { test('should handle special chars in doc path', () => {
expect( expect(

View file

@ -12,7 +12,11 @@ import {
} from './versions'; } from './versions';
import fs from 'fs-extra'; import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import {Sidebars, PathOptions, SidebarItem} from './types'; import {
PathOptions,
UnprocessedSidebarItem,
UnprocessedSidebars,
} from './types';
import {loadSidebars} from './sidebars'; import {loadSidebars} from './sidebars';
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants'; import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
@ -90,10 +94,14 @@ export function cliDocsVersionCommand(
// Load current sidebar and create a new versioned sidebars file. // Load current sidebar and create a new versioned sidebars file.
if (fs.existsSync(sidebarPath)) { if (fs.existsSync(sidebarPath)) {
const loadedSidebars: Sidebars = loadSidebars(sidebarPath); const loadedSidebars = loadSidebars(sidebarPath);
// TODO @slorber: this "version prefix" in versioned sidebars looks like a bad idea to me
// TODO try to get rid of it
// Transform id in original sidebar to versioned id. // Transform id in original sidebar to versioned id.
const normalizeItem = (item: SidebarItem): SidebarItem => { const normalizeItem = (
item: UnprocessedSidebarItem,
): UnprocessedSidebarItem => {
switch (item.type) { switch (item.type) {
case 'category': case 'category':
return {...item, items: item.items.map(normalizeItem)}; return {...item, items: item.items.map(normalizeItem)};
@ -108,14 +116,13 @@ export function cliDocsVersionCommand(
} }
}; };
const versionedSidebar: Sidebars = Object.entries(loadedSidebars).reduce( const versionedSidebar: UnprocessedSidebars = Object.entries(
(acc: Sidebars, [sidebarId, sidebarItems]) => { loadedSidebars,
const newVersionedSidebarId = `version-${version}/${sidebarId}`; ).reduce((acc: UnprocessedSidebars, [sidebarId, sidebarItems]) => {
acc[newVersionedSidebarId] = sidebarItems.map(normalizeItem); const newVersionedSidebarId = `version-${version}/${sidebarId}`;
return acc; acc[newVersionedSidebarId] = sidebarItems.map(normalizeItem);
}, return acc;
{}, }, {});
);
const versionedSidebarsDir = getVersionedSidebarsDirPath(siteDir, pluginId); const versionedSidebarsDir = getVersionedSidebarsDirPath(siteDir, pluginId);
const newSidebarFile = path.join( const newSidebarFile = path.join(

View file

@ -14,7 +14,9 @@ type DocFrontMatter = {
description?: string; description?: string;
slug?: string; slug?: string;
sidebar_label?: string; sidebar_label?: string;
sidebar_position?: number;
custom_edit_url?: string; custom_edit_url?: string;
strip_number_prefixes?: boolean;
}; };
const DocFrontMatterSchema = Joi.object<DocFrontMatter>({ const DocFrontMatterSchema = Joi.object<DocFrontMatter>({
@ -23,7 +25,9 @@ const DocFrontMatterSchema = Joi.object<DocFrontMatter>({
description: Joi.string(), description: Joi.string(),
slug: Joi.string(), slug: Joi.string(),
sidebar_label: Joi.string(), sidebar_label: Joi.string(),
sidebar_position: Joi.number(),
custom_edit_url: Joi.string().allow(null), custom_edit_url: Joi.string().allow(null),
strip_number_prefixes: Joi.boolean(),
}).unknown(); }).unknown();
export function assertDocFrontMatter( export function assertDocFrontMatter(

View file

@ -30,6 +30,7 @@ import getSlug from './slug';
import {CURRENT_VERSION_NAME} from './constants'; import {CURRENT_VERSION_NAME} from './constants';
import globby from 'globby'; import globby from 'globby';
import {getDocsDirPaths} from './versions'; import {getDocsDirPaths} from './versions';
import {extractNumberPrefix, stripPathNumberPrefixes} from './numberPrefix';
import {assertDocFrontMatter} from './docFrontMatter'; import {assertDocFrontMatter} from './docFrontMatter';
type LastUpdateOptions = Pick< type LastUpdateOptions = Pick<
@ -121,37 +122,66 @@ export function processDocMetadata({
}); });
assertDocFrontMatter(frontMatter); assertDocFrontMatter(frontMatter);
// ex: api/myDoc -> api
// ex: myDoc -> .
const docsFileDirName = path.dirname(source);
const { const {
sidebar_label: sidebarLabel, sidebar_label: sidebarLabel,
custom_edit_url: customEditURL, custom_edit_url: customEditURL,
// Strip number prefixes by default (01-MyFolder/01-MyDoc.md => MyFolder/MyDoc) by default,
// but ability to disable this behavior with frontmatterr
strip_number_prefixes: stripNumberPrefixes = true,
} = frontMatter; } = frontMatter;
const baseID: string = // ex: api/plugins/myDoc -> myDoc
frontMatter.id || path.basename(source, path.extname(source)); // ex: myDoc -> myDoc
const sourceFileNameWithoutExtension = path.basename(
source,
path.extname(source),
);
// ex: api/plugins/myDoc -> api/plugins
// ex: myDoc -> .
const sourceDirName = path.dirname(source);
const {filename: unprefixedFileName, numberPrefix} = stripNumberPrefixes
? extractNumberPrefix(sourceFileNameWithoutExtension)
: {filename: sourceFileNameWithoutExtension, numberPrefix: undefined};
const baseID: string = frontMatter.id ?? unprefixedFileName;
if (baseID.includes('/')) { if (baseID.includes('/')) {
throw new Error(`Document id [${baseID}] cannot include "/".`); throw new Error(`Document id [${baseID}] cannot include "/".`);
} }
// For autogenerated sidebars, sidebar position can come from filename number prefix or frontmatter
const sidebarPosition: number | undefined =
frontMatter.sidebar_position ?? numberPrefix;
// TODO legacy retrocompatibility // TODO legacy retrocompatibility
// The same doc in 2 distinct version could keep the same id, // The same doc in 2 distinct version could keep the same id,
// we just need to namespace the data by version // we just need to namespace the data by version
const versionIdPart = const versionIdPrefix =
versionMetadata.versionName === CURRENT_VERSION_NAME versionMetadata.versionName === CURRENT_VERSION_NAME
? '' ? undefined
: `version-${versionMetadata.versionName}/`; : `version-${versionMetadata.versionName}`;
// TODO legacy retrocompatibility // TODO legacy retrocompatibility
// I think it's bad to affect the frontmatter id with the dirname // I think it's bad to affect the frontmatter id with the dirname?
const dirNameIdPart = docsFileDirName === '.' ? '' : `${docsFileDirName}/`; function computeDirNameIdPrefix() {
if (sourceDirName === '.') {
return undefined;
}
// Eventually remove the number prefixes from intermediate directories
return stripNumberPrefixes
? stripPathNumberPrefixes(sourceDirName)
: sourceDirName;
}
// TODO legacy composite id, requires a breaking change to modify this const unversionedId = [computeDirNameIdPrefix(), baseID]
const id = `${versionIdPart}${dirNameIdPart}${baseID}`; .filter(Boolean)
.join('/');
const unversionedId = `${dirNameIdPart}${baseID}`; // TODO is versioning the id very useful in practice?
// legacy versioned id, requires a breaking change to modify this
const id = [versionIdPrefix, unversionedId].filter(Boolean).join('/');
// TODO remove soon, deprecated homePageId // TODO remove soon, deprecated homePageId
const isDocsHomePage = unversionedId === (homePageId ?? '_index'); const isDocsHomePage = unversionedId === (homePageId ?? '_index');
@ -165,8 +195,9 @@ export function processDocMetadata({
? '/' ? '/'
: getSlug({ : getSlug({
baseID, baseID,
dirName: docsFileDirName, dirName: sourceDirName,
frontmatterSlug: frontMatter.slug, frontmatterSlug: frontMatter.slug,
stripDirNumberPrefixes: stripNumberPrefixes,
}); });
// Default title is the id. // Default title is the id.
@ -212,6 +243,7 @@ export function processDocMetadata({
title, title,
description, description,
source: aliasedSitePath(filePath, siteDir), source: aliasedSitePath(filePath, siteDir),
sourceDirName,
slug: docSlug, slug: docSlug,
permalink, permalink,
editUrl: customEditURL !== undefined ? customEditURL : getDocEditUrl(), editUrl: customEditURL !== undefined ? customEditURL : getDocEditUrl(),
@ -224,6 +256,7 @@ export function processDocMetadata({
) )
: undefined, : undefined,
sidebar_label: sidebarLabel, sidebar_label: sidebarLabel,
sidebarPosition,
frontMatter, frontMatter,
}; };
} }

View file

@ -20,8 +20,7 @@ import {
addTrailingPathSeparator, addTrailingPathSeparator,
} from '@docusaurus/utils'; } from '@docusaurus/utils';
import {LoadContext, Plugin, RouteConfig} from '@docusaurus/types'; import {LoadContext, Plugin, RouteConfig} from '@docusaurus/types';
import {loadSidebars, createSidebarsUtils, processSidebars} from './sidebars';
import {loadSidebars, createSidebarsUtils} from './sidebars';
import {readVersionDocs, processDocMetadata} from './docs'; import {readVersionDocs, processDocMetadata} from './docs';
import {getDocsDirPaths, readVersionsMetadata} from './versions'; import {getDocsDirPaths, readVersionsMetadata} from './versions';
@ -49,6 +48,7 @@ import {
translateLoadedContent, translateLoadedContent,
getLoadedContentTranslationFiles, getLoadedContentTranslationFiles,
} from './translations'; } from './translations';
import {CategoryMetadataFilenamePattern} from './sidebarItemsGenerator';
export default function pluginContentDocs( export default function pluginContentDocs(
context: LoadContext, context: LoadContext,
@ -127,6 +127,7 @@ export default function pluginContentDocs(
), ),
), ),
), ),
`${version.contentPath}/**/${CategoryMetadataFilenamePattern}`,
]; ];
} }
@ -162,8 +163,9 @@ export default function pluginContentDocs(
async function loadVersion( async function loadVersion(
versionMetadata: VersionMetadata, versionMetadata: VersionMetadata,
): Promise<LoadedVersion> { ): Promise<LoadedVersion> {
const sidebars = loadSidebars(versionMetadata.sidebarFilePath); const unprocessedSidebars = loadSidebars(
const sidebarsUtils = createSidebarsUtils(sidebars); versionMetadata.sidebarFilePath,
);
const docsBase: DocMetadataBase[] = await loadVersionDocsBase( const docsBase: DocMetadataBase[] = await loadVersionDocsBase(
versionMetadata, versionMetadata,
@ -173,6 +175,15 @@ export default function pluginContentDocs(
(doc) => doc.id, (doc) => doc.id,
); );
const sidebars = await processSidebars({
sidebarItemsGenerator: options.sidebarItemsGenerator,
unprocessedSidebars,
docs: docsBase,
version: versionMetadata,
});
const sidebarsUtils = createSidebarsUtils(sidebars);
const validDocIds = Object.keys(docsBaseById); const validDocIds = Object.keys(docsBaseById);
sidebarsUtils.checkSidebarsDocIds(validDocIds); sidebarsUtils.checkSidebarsDocIds(validDocIds);

View file

@ -0,0 +1,35 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const NumberPrefixRegex = /^(?<numberPrefix>\d+)(?<separator>\s*[-_.]+\s*)(?<suffix>.*)$/;
// 0-myDoc => myDoc
export function stripNumberPrefix(str: string) {
return NumberPrefixRegex.exec(str)?.groups?.suffix ?? str;
}
// 0-myFolder/0-mySubfolder/0-myDoc => myFolder/mySubfolder/myDoc
export function stripPathNumberPrefixes(path: string) {
return path.split('/').map(stripNumberPrefix).join('/');
}
// 0-myDoc => {filename: myDoc, numberPrefix: 0}
// 003 - myDoc => {filename: myDoc, numberPrefix: 3}
export function extractNumberPrefix(
filename: string,
): {filename: string; numberPrefix?: number} {
const match = NumberPrefixRegex.exec(filename);
const cleanFileName = match?.groups?.suffix ?? filename;
const numberPrefixString = match?.groups?.numberPrefix;
const numberPrefix = numberPrefixString
? parseInt(numberPrefixString, 10)
: undefined;
return {
filename: cleanFileName,
numberPrefix,
};
}

View file

@ -15,13 +15,15 @@ import {
import {OptionValidationContext, ValidationResult} from '@docusaurus/types'; import {OptionValidationContext, ValidationResult} from '@docusaurus/types';
import chalk from 'chalk'; import chalk from 'chalk';
import admonitions from 'remark-admonitions'; import admonitions from 'remark-admonitions';
import {DefaultSidebarItemsGenerator} from './sidebarItemsGenerator';
export const DEFAULT_OPTIONS: Omit<PluginOptions, 'id'> = { export const DEFAULT_OPTIONS: Omit<PluginOptions, 'id'> = {
path: 'docs', // Path to data on filesystem, relative to site dir. path: 'docs', // Path to data on filesystem, relative to site dir.
routeBasePath: 'docs', // URL Route. routeBasePath: 'docs', // URL Route.
homePageId: undefined, // TODO remove soon, deprecated homePageId: undefined, // TODO remove soon, deprecated
include: ['**/*.{md,mdx}'], // Extensions to include. include: ['**/*.{md,mdx}'], // Extensions to include.
sidebarPath: 'sidebars.json', // Path to sidebar configuration for showing a list of markdown pages. sidebarPath: 'sidebars.json', // Path to the sidebars configuration file
sidebarItemsGenerator: DefaultSidebarItemsGenerator,
docLayoutComponent: '@theme/DocPage', docLayoutComponent: '@theme/DocPage',
docItemComponent: '@theme/DocItem', docItemComponent: '@theme/DocItem',
remarkPlugins: [], remarkPlugins: [],
@ -61,6 +63,9 @@ export const OptionsSchema = Joi.object({
homePageId: Joi.string().optional(), homePageId: Joi.string().optional(),
include: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.include), include: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.include),
sidebarPath: Joi.string().allow('').default(DEFAULT_OPTIONS.sidebarPath), sidebarPath: Joi.string().allow('').default(DEFAULT_OPTIONS.sidebarPath),
sidebarItemsGenerator: Joi.function().default(
() => DEFAULT_OPTIONS.sidebarItemsGenerator,
),
docLayoutComponent: Joi.string().default(DEFAULT_OPTIONS.docLayoutComponent), docLayoutComponent: Joi.string().default(DEFAULT_OPTIONS.docLayoutComponent),
docItemComponent: Joi.string().default(DEFAULT_OPTIONS.docItemComponent), docItemComponent: Joi.string().default(DEFAULT_OPTIONS.docItemComponent),
remarkPlugins: RemarkPluginsSchema.default(DEFAULT_OPTIONS.remarkPlugins), remarkPlugins: RemarkPluginsSchema.default(DEFAULT_OPTIONS.remarkPlugins),

View file

@ -0,0 +1,305 @@
/**
* 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 {
SidebarItem,
SidebarItemDoc,
SidebarItemCategory,
SidebarItemsGenerator,
SidebarItemsGeneratorDoc,
} from './types';
import {sortBy, take, last, orderBy} from 'lodash';
import {addTrailingSlash, posixPath} from '@docusaurus/utils';
import {Joi} from '@docusaurus/utils-validation';
import {extractNumberPrefix} from './numberPrefix';
import chalk from 'chalk';
import path from 'path';
import fs from 'fs-extra';
import Yaml from 'js-yaml';
import {DefaultCategoryCollapsedValue} from './sidebars';
const BreadcrumbSeparator = '/';
export const CategoryMetadataFilenameBase = '_category_';
export const CategoryMetadataFilenamePattern = '_category_.{json,yml,yaml}';
export type CategoryMetadatasFile = {
label?: string;
position?: number;
collapsed?: boolean;
// TODO should we allow "items" here? how would this work? would an "autogenerated" type be allowed?
// This mkdocs plugin do something like that: https://github.com/lukasgeiter/mkdocs-awesome-pages-plugin/
// cf comment: https://github.com/facebook/docusaurus/issues/3464#issuecomment-784765199
};
type WithPosition = {position?: number};
type SidebarItemWithPosition = SidebarItem & WithPosition;
const CategoryMetadatasFileSchema = Joi.object<CategoryMetadatasFile>({
label: Joi.string().optional(),
position: Joi.number().optional(),
collapsed: Joi.boolean().optional(),
});
// TODO later if there is `CategoryFolder/index.md`, we may want to read the metadata as yaml on it
// see https://github.com/facebook/docusaurus/issues/3464#issuecomment-818670449
async function readCategoryMetadatasFile(
categoryDirPath: string,
): Promise<CategoryMetadatasFile | null> {
function assertCategoryMetadataFile(
content: unknown,
): asserts content is CategoryMetadatasFile {
Joi.attempt(content, CategoryMetadatasFileSchema);
}
async function tryReadFile(
fileNameWithExtension: string,
parse: (content: string) => unknown,
): Promise<CategoryMetadatasFile | null> {
// Simpler to use only posix paths for mocking file metadatas in tests
const filePath = posixPath(
path.join(categoryDirPath, fileNameWithExtension),
);
if (await fs.pathExists(filePath)) {
const contentString = await fs.readFile(filePath, {encoding: 'utf8'});
const unsafeContent: unknown = parse(contentString);
try {
assertCategoryMetadataFile(unsafeContent);
return unsafeContent;
} catch (e) {
console.error(
chalk.red(
`The docs sidebar category metadata file looks invalid!\nPath=${filePath}`,
),
);
throw e;
}
}
return null;
}
return (
(await tryReadFile(`${CategoryMetadataFilenameBase}.json`, JSON.parse)) ??
(await tryReadFile(`${CategoryMetadataFilenameBase}.yml`, Yaml.load)) ??
// eslint-disable-next-line no-return-await
(await tryReadFile(`${CategoryMetadataFilenameBase}.yaml`, Yaml.load))
);
}
// [...parents, tail]
function parseBreadcrumb(
breadcrumb: string[],
): {parents: string[]; tail: string} {
return {
parents: take(breadcrumb, breadcrumb.length - 1),
tail: last(breadcrumb)!,
};
}
// Comment for this feature: https://github.com/facebook/docusaurus/issues/3464#issuecomment-818670449
export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async function defaultSidebarItemsGenerator({
item,
docs: allDocs,
version,
}): Promise<SidebarItem[]> {
// Doc at the root of the autogenerated sidebar dir
function isRootDoc(doc: SidebarItemsGeneratorDoc) {
return doc.sourceDirName === item.dirName;
}
// Doc inside a subfolder of the autogenerated sidebar dir
function isCategoryDoc(doc: SidebarItemsGeneratorDoc) {
if (isRootDoc(doc)) {
return false;
}
return (
// autogen dir is . and doc is in subfolder
item.dirName === '.' ||
// autogen dir is not . and doc is in subfolder
// "api/myDoc" startsWith "api/" (note "api2/myDoc" is not included)
doc.sourceDirName.startsWith(addTrailingSlash(item.dirName))
);
}
function isInAutogeneratedDir(doc: SidebarItemsGeneratorDoc) {
return isRootDoc(doc) || isCategoryDoc(doc);
}
// autogenDir=a/b and docDir=a/b/c/d => returns c/d
// autogenDir=a/b and docDir=a/b => returns .
function getDocDirRelativeToAutogenDir(
doc: SidebarItemsGeneratorDoc,
): string {
if (!isInAutogeneratedDir(doc)) {
throw new Error(
'getDocDirRelativeToAutogenDir() can only be called for subdocs of the sidebar autogen dir',
);
}
// Is there a node API to compare 2 relative paths more easily?
// path.relative() does not give good results
if (item.dirName === '.') {
return doc.sourceDirName;
} else if (item.dirName === doc.sourceDirName) {
return '.';
} else {
return doc.sourceDirName.replace(addTrailingSlash(item.dirName), '');
}
}
// Get only docs in the autogen dir
// Sort by folder+filename at once
const docs = sortBy(allDocs.filter(isInAutogeneratedDir), (d) => d.source);
if (docs.length === 0) {
console.warn(
chalk.yellow(
`No docs found in dir ${item.dirName}: can't auto-generate a sidebar`,
),
);
}
function createDocSidebarItem(
doc: SidebarItemsGeneratorDoc,
): SidebarItemDoc & WithPosition {
return {
type: 'doc',
id: doc.id,
...(doc.frontMatter.sidebar_label && {
label: doc.frontMatter.sidebar_label,
}),
...(typeof doc.sidebarPosition !== 'undefined' && {
position: doc.sidebarPosition,
}),
};
}
async function createCategorySidebarItem({
breadcrumb,
}: {
breadcrumb: string[];
}): Promise<SidebarItemCategory & WithPosition> {
const categoryDirPath = path.join(
version.contentPath,
breadcrumb.join(BreadcrumbSeparator),
);
const categoryMetadatas = await readCategoryMetadatasFile(categoryDirPath);
const {tail} = parseBreadcrumb(breadcrumb);
const {filename, numberPrefix} = extractNumberPrefix(tail);
const position = categoryMetadatas?.position ?? numberPrefix;
return {
type: 'category',
label: categoryMetadatas?.label ?? filename,
items: [],
collapsed: categoryMetadatas?.collapsed ?? DefaultCategoryCollapsedValue,
...(typeof position !== 'undefined' && {position}),
};
}
// Not sure how to simplify this algorithm :/
async function autogenerateSidebarItems(): Promise<
SidebarItemWithPosition[]
> {
const sidebarItems: SidebarItem[] = []; // mutable result
const categoriesByBreadcrumb: Record<string, SidebarItemCategory> = {}; // mutable cache of categories already created
async function getOrCreateCategoriesForBreadcrumb(
breadcrumb: string[],
): Promise<SidebarItemCategory | null> {
if (breadcrumb.length === 0) {
return null;
}
const {parents} = parseBreadcrumb(breadcrumb);
const parentCategory = await getOrCreateCategoriesForBreadcrumb(parents);
const existingCategory =
categoriesByBreadcrumb[breadcrumb.join(BreadcrumbSeparator)];
if (existingCategory) {
return existingCategory;
} else {
const newCategory = await createCategorySidebarItem({
breadcrumb,
});
if (parentCategory) {
parentCategory.items.push(newCategory);
} else {
sidebarItems.push(newCategory);
}
categoriesByBreadcrumb[
breadcrumb.join(BreadcrumbSeparator)
] = newCategory;
return newCategory;
}
}
// Get the category breadcrumb of a doc (relative to the dir of the autogenerated sidebar item)
function getRelativeBreadcrumb(doc: SidebarItemsGeneratorDoc): string[] {
const relativeDirPath = getDocDirRelativeToAutogenDir(doc);
if (relativeDirPath === '.') {
return [];
} else {
return relativeDirPath.split(BreadcrumbSeparator);
}
}
async function handleDocItem(doc: SidebarItemsGeneratorDoc): Promise<void> {
const breadcrumb = getRelativeBreadcrumb(doc);
const category = await getOrCreateCategoriesForBreadcrumb(breadcrumb);
const docSidebarItem = createDocSidebarItem(doc);
if (category) {
category.items.push(docSidebarItem);
} else {
sidebarItems.push(docSidebarItem);
}
}
// async process made sequential on purpose! order matters
for (const doc of docs) {
// eslint-disable-next-line no-await-in-loop
await handleDocItem(doc);
}
return sidebarItems;
}
const sidebarItems = await autogenerateSidebarItems();
return sortSidebarItems(sidebarItems);
};
// Recursively sort the categories/docs + remove the "position" attribute from final output
// Note: the "position" is only used to sort "inside" a sidebar slice
// It is not used to sort across multiple consecutive sidebar slices (ie a whole Category composed of multiple autogenerated items)
function sortSidebarItems(
sidebarItems: SidebarItemWithPosition[],
): SidebarItem[] {
const processedSidebarItems = sidebarItems.map((item) => {
if (item.type === 'category') {
return {
...item,
items: sortSidebarItems(item.items),
};
}
return item;
});
const sortedSidebarItems = orderBy(
processedSidebarItems,
(item) => item.position,
['asc'],
);
return sortedSidebarItems.map(({position: _removed, ...item}) => item);
}

View file

@ -16,9 +16,18 @@ import {
Sidebar, Sidebar,
SidebarItemCategory, SidebarItemCategory,
SidebarItemType, SidebarItemType,
UnprocessedSidebarItem,
UnprocessedSidebars,
UnprocessedSidebar,
DocMetadataBase,
VersionMetadata,
SidebarItemsGenerator,
SidebarItemsGeneratorDoc,
SidebarItemsGeneratorVersion,
} from './types'; } from './types';
import {mapValues, flatten, flatMap, difference} from 'lodash'; import {mapValues, flatten, flatMap, difference, pick, memoize} from 'lodash';
import {getElementsAround} from '@docusaurus/utils'; import {getElementsAround} from '@docusaurus/utils';
import combinePromises from 'combine-promises';
type SidebarItemCategoryJSON = SidebarItemBase & { type SidebarItemCategoryJSON = SidebarItemBase & {
type: 'category'; type: 'category';
@ -27,12 +36,18 @@ type SidebarItemCategoryJSON = SidebarItemBase & {
collapsed?: boolean; collapsed?: boolean;
}; };
type SidebarItemAutogeneratedJSON = SidebarItemBase & {
type: 'autogenerated';
dirName: string;
};
type SidebarItemJSON = type SidebarItemJSON =
| string | string
| SidebarCategoryShorthandJSON | SidebarCategoryShorthandJSON
| SidebarItemDoc | SidebarItemDoc
| SidebarItemLink | SidebarItemLink
| SidebarItemCategoryJSON | SidebarItemCategoryJSON
| SidebarItemAutogeneratedJSON
| { | {
type: string; type: string;
[key: string]: unknown; [key: string]: unknown;
@ -56,7 +71,7 @@ function isCategoryShorthand(
} }
// categories are collapsed by default, unless user set collapsed = false // categories are collapsed by default, unless user set collapsed = false
const defaultCategoryCollapsedValue = true; export const DefaultCategoryCollapsedValue = true;
/** /**
* Convert {category1: [item1,item2]} shorthand syntax to long-form syntax * Convert {category1: [item1,item2]} shorthand syntax to long-form syntax
@ -66,7 +81,7 @@ function normalizeCategoryShorthand(
): SidebarItemCategoryJSON[] { ): SidebarItemCategoryJSON[] {
return Object.entries(sidebar).map(([label, items]) => ({ return Object.entries(sidebar).map(([label, items]) => ({
type: 'category', type: 'category',
collapsed: defaultCategoryCollapsedValue, collapsed: DefaultCategoryCollapsedValue,
label, label,
items, items,
})); }));
@ -78,7 +93,7 @@ function normalizeCategoryShorthand(
function assertItem<K extends string>( function assertItem<K extends string>(
item: Record<string, unknown>, item: Record<string, unknown>,
keys: K[], keys: K[],
): asserts item is Record<K, never> { ): asserts item is Record<K, unknown> {
const unknownKeys = Object.keys(item).filter( const unknownKeys = Object.keys(item).filter(
// @ts-expect-error: key is always string // @ts-expect-error: key is always string
(key) => !keys.includes(key as string) && key !== 'type', (key) => !keys.includes(key as string) && key !== 'type',
@ -115,6 +130,24 @@ function assertIsCategory(
} }
} }
function assertIsAutogenerated(
item: Record<string, unknown>,
): asserts item is SidebarItemAutogeneratedJSON {
assertItem(item, ['dirName', 'customProps']);
if (typeof item.dirName !== 'string') {
throw new Error(
`Error loading ${JSON.stringify(item)}. "dirName" must be a string.`,
);
}
if (item.dirName.startsWith('/') || item.dirName.endsWith('/')) {
throw new Error(
`Error loading ${JSON.stringify(
item,
)}. "dirName" must be a dir path relative to the docs folder root, and should not start or end with /`,
);
}
}
function assertIsDoc( function assertIsDoc(
item: Record<string, unknown>, item: Record<string, unknown>,
): asserts item is SidebarItemDoc { ): asserts item is SidebarItemDoc {
@ -152,7 +185,7 @@ function assertIsLink(
* Normalizes recursively item and all its children. Ensures that at the end * Normalizes recursively item and all its children. Ensures that at the end
* each item will be an object with the corresponding type. * each item will be an object with the corresponding type.
*/ */
function normalizeItem(item: SidebarItemJSON): SidebarItem[] { function normalizeItem(item: SidebarItemJSON): UnprocessedSidebarItem[] {
if (typeof item === 'string') { if (typeof item === 'string') {
return [ return [
{ {
@ -169,11 +202,14 @@ function normalizeItem(item: SidebarItemJSON): SidebarItem[] {
assertIsCategory(item); assertIsCategory(item);
return [ return [
{ {
collapsed: defaultCategoryCollapsedValue, collapsed: DefaultCategoryCollapsedValue,
...item, ...item,
items: flatMap(item.items, normalizeItem), items: flatMap(item.items, normalizeItem),
}, },
]; ];
case 'autogenerated':
assertIsAutogenerated(item);
return [item];
case 'link': case 'link':
assertIsLink(item); assertIsLink(item);
return [item]; return [item];
@ -195,7 +231,7 @@ function normalizeItem(item: SidebarItemJSON): SidebarItem[] {
} }
} }
function normalizeSidebar(sidebar: SidebarJSON) { function normalizeSidebar(sidebar: SidebarJSON): UnprocessedSidebar {
const normalizedSidebar: SidebarItemJSON[] = Array.isArray(sidebar) const normalizedSidebar: SidebarItemJSON[] = Array.isArray(sidebar)
? sidebar ? sidebar
: normalizeCategoryShorthand(sidebar); : normalizeCategoryShorthand(sidebar);
@ -203,21 +239,29 @@ function normalizeSidebar(sidebar: SidebarJSON) {
return flatMap(normalizedSidebar, normalizeItem); return flatMap(normalizedSidebar, normalizeItem);
} }
function normalizeSidebars(sidebars: SidebarsJSON): Sidebars { function normalizeSidebars(sidebars: SidebarsJSON): UnprocessedSidebars {
return mapValues(sidebars, normalizeSidebar); return mapValues(sidebars, normalizeSidebar);
} }
export const DefaultSidebars: UnprocessedSidebars = {
defaultSidebar: [
{
type: 'autogenerated',
dirName: '.',
},
],
};
// TODO refactor: make async // TODO refactor: make async
export function loadSidebars(sidebarFilePath: string): Sidebars { export function loadSidebars(sidebarFilePath: string): UnprocessedSidebars {
if (!sidebarFilePath) { if (!sidebarFilePath) {
throw new Error(`sidebarFilePath not provided: ${sidebarFilePath}`); throw new Error(`sidebarFilePath not provided: ${sidebarFilePath}`);
} }
// sidebars file is optional, some users use docs without sidebars! // No sidebars file: by default we use the file-system structure to generate the sidebar
// See https://github.com/facebook/docusaurus/issues/3366 // See https://github.com/facebook/docusaurus/pull/4582
if (!fs.existsSync(sidebarFilePath)) { if (!fs.existsSync(sidebarFilePath)) {
// throw new Error(`No sidebar file exist at path: ${sidebarFilePath}`); return DefaultSidebars;
return {};
} }
// We don't want sidebars to be cached because of hot reloading. // We don't want sidebars to be cached because of hot reloading.
@ -225,6 +269,87 @@ export function loadSidebars(sidebarFilePath: string): Sidebars {
return normalizeSidebars(sidebarJson); return normalizeSidebars(sidebarJson);
} }
export function toSidebarItemsGeneratorDoc(
doc: DocMetadataBase,
): SidebarItemsGeneratorDoc {
return pick(doc, [
'id',
'frontMatter',
'source',
'sourceDirName',
'sidebarPosition',
]);
}
export function toSidebarItemsGeneratorVersion(
version: VersionMetadata,
): SidebarItemsGeneratorVersion {
return pick(version, ['versionName', 'contentPath']);
}
// Handle the generation of autogenerated sidebar items
export async function processSidebar({
sidebarItemsGenerator,
unprocessedSidebar,
docs,
version,
}: {
sidebarItemsGenerator: SidebarItemsGenerator;
unprocessedSidebar: UnprocessedSidebar;
docs: DocMetadataBase[];
version: VersionMetadata;
}): Promise<Sidebar> {
// Just a minor lazy transformation optimization
const getSidebarItemsGeneratorDocsAndVersion = memoize(() => ({
docs: docs.map(toSidebarItemsGeneratorDoc),
version: toSidebarItemsGeneratorVersion(version),
}));
async function processRecursive(
item: UnprocessedSidebarItem,
): Promise<SidebarItem[]> {
if (item.type === 'category') {
return [
{
...item,
items: (await Promise.all(item.items.map(processRecursive))).flat(),
},
];
}
if (item.type === 'autogenerated') {
return sidebarItemsGenerator({
item,
...getSidebarItemsGeneratorDocsAndVersion(),
});
}
return [item];
}
return (await Promise.all(unprocessedSidebar.map(processRecursive))).flat();
}
export async function processSidebars({
sidebarItemsGenerator,
unprocessedSidebars,
docs,
version,
}: {
sidebarItemsGenerator: SidebarItemsGenerator;
unprocessedSidebars: UnprocessedSidebars;
docs: DocMetadataBase[];
version: VersionMetadata;
}): Promise<Sidebars> {
return combinePromises(
mapValues(unprocessedSidebars, (unprocessedSidebar) =>
processSidebar({
sidebarItemsGenerator,
unprocessedSidebar,
docs,
version,
}),
),
);
}
function collectSidebarItemsOfType< function collectSidebarItemsOfType<
Type extends SidebarItemType, Type extends SidebarItemType,
Item extends SidebarItem & {type: SidebarItemType} Item extends SidebarItem & {type: SidebarItemType}

View file

@ -11,23 +11,31 @@ import {
isValidPathname, isValidPathname,
resolvePathname, resolvePathname,
} from '@docusaurus/utils'; } from '@docusaurus/utils';
import {stripPathNumberPrefixes} from './numberPrefix';
export default function getSlug({ export default function getSlug({
baseID, baseID,
frontmatterSlug, frontmatterSlug,
dirName, dirName,
stripDirNumberPrefixes = true,
}: { }: {
baseID: string; baseID: string;
frontmatterSlug?: string; frontmatterSlug?: string;
dirName: string; dirName: string;
stripDirNumberPrefixes?: boolean;
}): string { }): string {
const baseSlug = frontmatterSlug || baseID; const baseSlug = frontmatterSlug || baseID;
let slug: string; let slug: string;
if (baseSlug.startsWith('/')) { if (baseSlug.startsWith('/')) {
slug = baseSlug; slug = baseSlug;
} else { } else {
const dirNameStripped = stripDirNumberPrefixes
? stripPathNumberPrefixes(dirName)
: dirName;
const resolveDirname = const resolveDirname =
dirName === '.' ? '/' : addLeadingSlash(addTrailingSlash(dirName)); dirName === '.'
? '/'
: addLeadingSlash(addTrailingSlash(dirNameStripped));
slug = resolvePathname(baseSlug, resolveDirname); slug = resolvePathname(baseSlug, resolveDirname);
} }

View file

@ -83,6 +83,7 @@ export type PluginOptions = MetadataOptions &
disableVersioning: boolean; disableVersioning: boolean;
excludeNextVersionDocs?: boolean; excludeNextVersionDocs?: boolean;
includeCurrentVersion: boolean; includeCurrentVersion: boolean;
sidebarItemsGenerator: SidebarItemsGenerator;
}; };
export type SidebarItemBase = { export type SidebarItemBase = {
@ -108,6 +109,27 @@ export type SidebarItemCategory = SidebarItemBase & {
collapsed: boolean; collapsed: boolean;
}; };
export type UnprocessedSidebarItemAutogenerated = {
type: 'autogenerated';
dirName: string;
};
export type UnprocessedSidebarItemCategory = SidebarItemBase & {
type: 'category';
label: string;
items: UnprocessedSidebarItem[];
collapsed: boolean;
};
export type UnprocessedSidebarItem =
| SidebarItemDoc
| SidebarItemLink
| UnprocessedSidebarItemCategory
| UnprocessedSidebarItemAutogenerated;
export type UnprocessedSidebar = UnprocessedSidebarItem[];
export type UnprocessedSidebars = Record<string, UnprocessedSidebar>;
export type SidebarItem = export type SidebarItem =
| SidebarItemDoc | SidebarItemDoc
| SidebarItemLink | SidebarItemLink
@ -115,9 +137,25 @@ export type SidebarItem =
export type Sidebar = SidebarItem[]; export type Sidebar = SidebarItem[];
export type SidebarItemType = SidebarItem['type']; export type SidebarItemType = SidebarItem['type'];
export type Sidebars = Record<string, Sidebar>; export type Sidebars = Record<string, Sidebar>;
// Reduce API surface for options.sidebarItemsGenerator
// The user-provided generator fn should receive only a subset of metadatas
// A change to any of these metadatas can be considered as a breaking change
export type SidebarItemsGeneratorDoc = Pick<
DocMetadataBase,
'id' | 'frontMatter' | 'source' | 'sourceDirName' | 'sidebarPosition'
>;
export type SidebarItemsGeneratorVersion = Pick<
VersionMetadata,
'versionName' | 'contentPath'
>;
export type SidebarItemsGenerator = (generatorArgs: {
item: UnprocessedSidebarItemAutogenerated;
version: SidebarItemsGeneratorVersion;
docs: SidebarItemsGeneratorDoc[];
}) => Promise<SidebarItem[]>;
export type OrderMetadata = { export type OrderMetadata = {
previous?: string; previous?: string;
next?: string; next?: string;
@ -143,10 +181,12 @@ export type DocMetadataBase = LastUpdateData & {
title: string; title: string;
description: string; description: string;
source: string; source: string;
sourceDirName: string; // relative to the docs folder (can be ".")
slug: string; slug: string;
permalink: string; permalink: string;
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
sidebar_label?: string; sidebar_label?: string;
sidebarPosition?: number;
editUrl?: string | null; editUrl?: string | null;
frontMatter: FrontMatter; frontMatter: FrontMatter;
}; };

View file

@ -434,6 +434,7 @@ function filterVersions(
} }
} }
// TODO make this async (requires plugin init to be async)
export function readVersionsMetadata({ export function readVersionsMetadata({
context, context,
options, options,

View file

@ -91,6 +91,7 @@ export default function CodeBlock({
children, children,
className: languageClassName, className: languageClassName,
metastring, metastring,
title,
}: Props): JSX.Element { }: Props): JSX.Element {
const {prism} = useThemeConfig(); const {prism} = useThemeConfig();
@ -107,9 +108,13 @@ export default function CodeBlock({
setMounted(true); setMounted(true);
}, []); }, []);
// TODO: the title is provided by MDX as props automatically
// so we probably don't need to parse the metastring
// (note: title="xyz" => title prop still has the quotes)
const codeBlockTitle = parseCodeBlockTitle(metastring) || title;
const button = useRef(null); const button = useRef(null);
let highlightLines: number[] = []; let highlightLines: number[] = [];
const codeBlockTitle = parseCodeBlockTitle(metastring);
const prismTheme = usePrismTheme(); const prismTheme = usePrismTheme();

View file

@ -55,6 +55,7 @@ declare module '@theme/CodeBlock' {
readonly children: string; readonly children: string;
readonly className?: string; readonly className?: string;
readonly metastring?: string; readonly metastring?: string;
readonly title?: string;
}; };
const CodeBlock: (props: Props) => JSX.Element; const CodeBlock: (props: Props) => JSX.Element;

View file

@ -10,6 +10,9 @@ const path = require('path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const {mapValues, pickBy} = require('lodash'); const {mapValues, pickBy} = require('lodash');
// Seems the 5s default timeout fails sometimes
jest.setTimeout(15000);
describe('update-code-translations', () => { describe('update-code-translations', () => {
test(`to have base.json contain all the translations extracted from the theme. Please run "yarn workspace @docusaurus/theme-classic update-code-translations" to keep base.json up-to-date.`, async () => { test(`to have base.json contain all the translations extracted from the theme. Please run "yarn workspace @docusaurus/theme-classic update-code-translations" to keep base.json up-to-date.`, async () => {
const baseMessages = pickBy( const baseMessages = pickBy(

View file

@ -1,8 +1,4 @@
--- # Support
id: support
title: Support
slug: /support
---
Docusaurus has a community of thousands of developers. Docusaurus has a community of thousands of developers.

View file

@ -1,8 +1,4 @@
--- # Team
id: team
title: Team
slug: /team
---
import { import {
ActiveTeamRow, ActiveTeamRow,

View file

@ -1,8 +1,4 @@
--- # Awesome Resources
id: resources
title: Awesome Resources
slug: /resources
---
A curated list of interesting Docusaurus community projects. A curated list of interesting Docusaurus community projects.

View file

@ -70,9 +70,16 @@ module.exports = {
include: ['**/*.md', '**/*.mdx'], // Extensions to include. include: ['**/*.md', '**/*.mdx'], // Extensions to include.
/** /**
* Path to sidebar configuration for showing a list of markdown pages. * Path to sidebar configuration for showing a list of markdown pages.
* Warning: will change
*/ */
sidebarPath: '', sidebarPath: 'sidebars.js',
/**
* Function used to replace the sidebar items of type "autogenerated"
* by real sidebar items (docs, categories, links...)
*/
sidebarItemsGenerator: function ({item, version, docs}) {
// Use the provided data to create a custom "sidebar slice"
return [{type: 'doc', id: 'doc1'}];
},
/** /**
* Theme components used by the docs pages * Theme components used by the docs pages
*/ */
@ -154,14 +161,16 @@ Markdown documents can use the following markdown frontmatter metadata fields, e
- `id`: A unique document id. If this field is not present, the document's `id` will default to its file name (without the extension) - `id`: A unique document id. If this field is not present, the document's `id` will default to its file name (without the extension)
- `title`: The title of your document. If this field is not present, the document's `title` will default to its `id` - `title`: The title of your document. If this field is not present, the document's `title` will default to its `id`
- `hide_title`: Whether to hide the title at the top of the doc. By default it is `false` - `hide_title`: Whether to hide the title at the top of the doc. By default, it is `false`
- `hide_table_of_contents`: Whether to hide the table of contents to the right. By default it is `false` - `hide_table_of_contents`: Whether to hide the table of contents to the right. By default it is `false`
- `sidebar_label`: The text shown in the document sidebar and in the next/previous button for this document. If this field is not present, the document's `sidebar_label` will default to its `title` - `sidebar_label`: The text shown in the document sidebar and in the next/previous button for this document. If this field is not present, the document's `sidebar_label` will default to its `title`
- `sidebar_position`: Permits to control the position of a doc inside the generated sidebar slice, when using `autogenerated` sidebar items. Can be Int or Float.
- `strip_number_prefixes`: When a document has a number prefix (`001 - My Doc.md`, `2. MyDoc.md`...), it is automatically removed, and the prefix is used as `sidebar_position`. Use `strip_number_prefixes: false` if you want to disable this behavior
- `custom_edit_url`: The URL for editing this document. If this field is not present, the document's edit URL will fall back to `editUrl` from options fields passed to `docusaurus-plugin-content-docs` - `custom_edit_url`: The URL for editing this document. If this field is not present, the document's edit URL will fall back to `editUrl` from options fields passed to `docusaurus-plugin-content-docs`
- `keywords`: Keywords meta tag for the document page, for search engines - `keywords`: Keywords meta tag for the document page, for search engines
- `description`: The description of your document, which will become the `<meta name="description" content="..."/>` and `<meta property="og:description" content="..."/>` in `<head>`, used by search engines. If this field is not present, it will default to the first line of the contents - `description`: The description of your document, which will become the `<meta name="description" content="..."/>` and `<meta property="og:description" content="..."/>` in `<head>`, used by search engines. If this field is not present, it will default to the first line of the contents
- `image`: Cover or thumbnail image that will be used when displaying the link to your post - `image`: Cover or thumbnail image that will be used when displaying the link to your post
- `slug`: Allows to customize the document url - `slug`: Allows to customize the document url (`/<routeBasePath>/<slug>`). Support multiple patterns: `slug: my-doc`, `slug: /my/path/myDoc`, `slug: /`
Example: Example:

View file

@ -4,12 +4,20 @@ title: Creating Pages
slug: /creating-pages slug: /creating-pages
--- ---
In this section, we will learn about creating ad-hoc pages in Docusaurus using React. This is most useful for creating one-off standalone pages like a showcase page, playground page or support page. In this section, we will learn about creating pages in Docusaurus.
This is useful for creating **one-off standalone pages** like a showcase page, playground page or support page.
The functionality of pages is powered by `@docusaurus/plugin-content-pages`. The functionality of pages is powered by `@docusaurus/plugin-content-pages`.
You can use React components, or Markdown. You can use React components, or Markdown.
:::note
Pages do not have sidebars, only [docs](./docs/docs-introduction.md) have.
:::
## Add a React page {#add-a-react-page} ## Add a React page {#add-a-react-page}
Create a file `/src/pages/helloReact.js`: Create a file `/src/pages/helloReact.js`:

View file

@ -4,170 +4,241 @@ title: Sidebar
slug: /sidebar slug: /sidebar
--- ---
To generate a sidebar to your Docusaurus site: Creating a sidebar is useful to:
- Group multiple **related documents**
- **Display a sidebar** on each of those documents
- Provide a **paginated navigation**, with next/previous button
To use sidebars on your Docusaurus site:
1. Define a file that exports a [sidebar object](#sidebar-object). 1. Define a file that exports a [sidebar object](#sidebar-object).
1. Pass this object into the `@docusaurus/plugin-docs` plugin directly or via `@docusaurus/preset-classic`. 1. Pass this object into the `@docusaurus/plugin-docs` plugin directly or via `@docusaurus/preset-classic`.
```js {8-9} title="docusaurus.config.js" ```js title="docusaurus.config.js"
module.exports = { module.exports = {
// ...
presets: [ presets: [
[ [
'@docusaurus/preset-classic', '@docusaurus/preset-classic',
{ {
docs: { docs: {
// Sidebars filepath relative to the site dir. // highlight-start
sidebarPath: require.resolve('./sidebars.js'), sidebarPath: require.resolve('./sidebars.js'),
// highlight-end
}, },
// ...
}, },
], ],
], ],
}; };
``` ```
## Sidebar object {#sidebar-object} ## Default sidebar
A sidebar object contains [sidebar items](#understanding-sidebar-items) and it is defined like this: By default, Docusaurus [automatically generates a sidebar](#sidebar-item-autogenerated) for you, by using the filesystem structure of the `docs` folder:
```typescript
type Sidebar = {
[sidebarId: string]:
| {
[sidebarCategory: string]: SidebarItem[];
}
| SidebarItem[];
};
```
For example:
```js title="sidebars.js" ```js title="sidebars.js"
module.exports = { module.exports = {
docs: [ mySidebar: [
{ {
type: 'category', type: 'autogenerated',
label: 'Getting Started', dirName: '.', // generate sidebar slice from the docs folder (or versioned_docs/<version>)
items: ['greeting'],
},
{
type: 'category',
label: 'Docusaurus',
items: ['doc1'],
}, },
], ],
}; };
``` ```
In this example, notice the following: You can also define your sidebars explicitly.
- The key `docs` is the id of the sidebar. The id can be any value, not necessarily `docs`. ## Sidebar object {#sidebar-object}
- `Getting Started` is a category within the sidebar.
- `greeting` and `doc1` are both [sidebar item](#understanding-sidebar-items).
Shorthand notation can also be used: A sidebar is a **tree of [sidebar items](#understanding-sidebar-items)**.
```typescript
type Sidebar =
// Normal syntax
| SidebarItem[]
// Shorthand syntax
| Record<
string, // category label
SidebarItem[] // category items
>;
```
A sidebars file can contain **multiple sidebar objects**.
```typescript
type SidebarsFile = Record<
string, // sidebar id
Sidebar
>;
```
Example:
```js title="sidebars.js" ```js title="sidebars.js"
module.exports = { module.exports = {
docs: { mySidebar: [
'Getting started': ['greeting'], {
Docusaurus: ['doc1'], type: 'category',
}, label: 'Getting Started',
items: ['doc1'],
},
{
type: 'category',
label: 'Docusaurus',
items: ['doc2', 'doc3'],
},
],
}; };
``` ```
:::note Notice the following:
Shorthand notation relies on the iteration order of JavaScript object keys for the category name. When using this notation, keep in mind that EcmaScript does not guarantee `Object.keys({a,b}) === ['a','b']`, yet this is generally true. - There is a single sidebar `mySidebar`, containing 5 [sidebar items](#understanding-sidebar-items)
- `Getting Started` and `Docusaurus` are sidebar categories
- `doc1`, `doc2` and `doc3` are sidebar documents
:::tip
Use the **shorthand syntax** to express this sidebar more concisely:
```js title="sidebars.js"
module.exports = {
mySidebar: {
'Getting started': ['doc1'],
Docusaurus: ['doc2', 'doc3'],
},
};
```
::: :::
## Using multiple sidebars {#using-multiple-sidebars} ## Using multiple sidebars {#using-multiple-sidebars}
You can have multiple sidebars for different Markdown files by adding more top-level keys to the exported object. You can create a sidebar for each **set of markdown files** that you want to **group together**.
:::tip
The Docusaurus site is a good example of using multiple sidebars:
- [Docs](../../introduction.md)
- [API](../../cli.md)
:::
Example: Example:
```js title="sidebars.js" ```js title="sidebars.js"
module.exports = { module.exports = {
firstSidebar: { tutorialSidebar: {
'Category A': ['doc1'], 'Category A': ['doc1', 'doc2'],
},
secondSidebar: {
'Category A': ['doc2'],
'Category B': ['doc3'],
}, },
apiSidebar: ['doc3', 'doc4'],
}; };
``` ```
By default, the doc page the user is reading will display the sidebar that it is part of. This can be customized with the [sidebar type](#understanding-sidebar-items). :::note
For example, with the above example, when displaying the `doc2` page, the sidebar will contain the items of `secondSidebar` only. Another example of multiple sidebars is the `API` and `Docs` sections on the Docusaurus website. The keys `tutorialSidebar` and `apiSidebar` are sidebar **technical ids** and do not matter much.
For more information about sidebars and how they relate to doc pages, see [Navbar doc link](../../api/themes/theme-configuration.md#navbar-doc-link). :::
When browsing:
- `doc1` or `doc2`: the `tutorialSidebar` will be displayed
- `doc3` or `doc4`: the `apiSidebar` will be displayed
A **paginated navigation** link documents inside the same sidebar with **next and previous buttons**.
## Understanding sidebar items {#understanding-sidebar-items} ## Understanding sidebar items {#understanding-sidebar-items}
As the name implies, `SidebarItem` is an item defined in a Sidebar. A SidebarItem as a `type` that defines what the item links to. `SidebarItem` is an item defined in a Sidebar tree.
`type` supports the following values There are different types of sidebar items:
- [Doc](#linking-to-a-doc-page) - **[Doc](#sidebar-item-doc)**: link to a doc page, assigning it to the sidebar
- [Link](#creating-a-generic-link) - **[Ref](#sidebar-item-ref)**: link to a doc page, without assigning it to the sidebar
- [Ref](#creating-a-link-to-page-without-sidebar) - **[Link](#sidebar-item-link)**: link to any internal or external page
- [Category](#creating-a-hierarchy) - **[Category](#sidebar-item-category)**: create a hierarchy of sidebar items
- **[Autogenerated](#sidebar-item-autogenerated)**: generate a sidebar slice automatically
### Linking to a doc page {#linking-to-a-doc-page} ### Doc: link to a doc {#sidebar-item-doc}
Set `type` to `doc` to link to a documentation page. This is the default type. Use the `doc` type to link to a doc page and assign that doc to a sidebar:
```typescript ```typescript
type SidebarItemDoc = type SidebarItemDoc =
| string // Normal syntax
| { | {
type: 'doc'; type: 'doc';
id: string; id: string;
label: string; // Sidebar label text label: string; // Sidebar label text
}; }
// Shorthand syntax
| string; // docId shortcut
``` ```
Example: Example:
```js
{
type: 'doc',
id: 'doc1', // string - document id
label: 'Getting started' // Sidebar label text
}
```
The `sidebar_label` in the markdown frontmatter has a higher precedence over the `label` key in `SidebarItemDoc`. Using just the [Document ID](#document-id) is also valid, the following is equivalent to the above:
```js
'doc1'; // string - document id
```
Using this type will bind the linked doc to current sidebar. This means that if you access the `doc1` page, the sidebar displayed will be the sidebar that contains this doc page.
In the example below, `doc1` is bound to `firstSidebar`.
```js title="sidebars.js" ```js title="sidebars.js"
module.exports = { module.exports = {
firstSidebar: { mySidebar: [
'Category A': ['doc1'], // Normal syntax:
}, // highlight-start
secondSidebar: { {
'Category A': ['doc2'], type: 'doc',
'Category B': ['doc3'], id: 'doc1', // document id
}, label: 'Getting started', // sidebar label
},
// highlight-end
// Shorthand syntax:
// highlight-start
'doc2', // document id
// highlight-end
],
}; };
``` ```
### Creating a generic link {#creating-a-generic-link} The `sidebar_label` markdown frontmatter has a higher precedence over the `label` key in `SidebarItemDoc`.
Set `type` to `link` to link to a non-documentation page. For example, to create an external link. :::note
Don't assign the same doc to multiple sidebars: use a [ref](#sidebar-item-ref) instead.
:::
### Ref: link to a doc, without sidebar {#sidebar-item-ref}
Use the `ref` type to link to a doc page without assigning it to a sidebar.
```typescript
type SidebarItemRef = {
type: 'ref';
id: string;
};
```
Example:
```js title="sidebars.js"
module.exports = {
mySidebar: [
{
type: 'ref',
id: 'doc1', // Document id (string).
},
],
};
```
When browsing `doc1`, Docusaurus **will not display** the `mySidebar` sidebar.
### Link: link to any page {#sidebar-item-link}
Use the `link` type to link to any page (internal or external) that is not a doc.
```typescript ```typescript
type SidebarItemLink = { type SidebarItemLink = {
@ -179,43 +250,41 @@ type SidebarItemLink = {
Example: Example:
```js ```js title="sidebars.js"
{ module.exports = {
type: 'link', myLinksSidebar: [
label: 'Custom Label', // The label that should be displayed (string). // highlight-start
href: 'https://example.com' // The target URL (string). // External link
} {
``` type: 'link',
label: 'Facebook', // The link label
href: 'https://facebook.com', // The external URL
},
// highlight-end
### Creating a link to page without sidebar {#creating-a-link-to-page-without-sidebar} // highlight-start
// Internal link
Set `type` to `ref` to link to a documentation page without binding it to a sidebar. This means the sidebar disappears when the user displays the linked page. {
type: 'link',
```typescript label: 'Home', // The link label
type SidebarItemRef = { href: '/', // The internal path
type: 'ref'; },
id: string; // highlight-end
],
}; };
``` ```
Example: ### Category: create a hierarchy {#sidebar-item-category}
```js Use the `category` type to create a hierarchy of sidebar items.
{
type: 'ref',
id: 'doc1', // Document id (string).
}
```
### Creating a hierarchy {#creating-a-hierarchy}
The Sidebar item type that creates a hierarchy in the sidebar. Set `type` to `category`.
```typescript ```typescript
type SidebarItemCategory = { type SidebarItemCategory = {
type: 'category'; type: 'category';
label: string; // Sidebar label text. label: string; // Sidebar label text.
items: SidebarItem[]; // Array of sidebar items. items: SidebarItem[]; // Array of sidebar items.
// Category options:
collapsed: boolean; // Set the category to be collapsed or open by default collapsed: boolean; // Set the category to be collapsed or open by default
}; };
``` ```
@ -225,16 +294,16 @@ Example:
```js title="sidebars.js" ```js title="sidebars.js"
module.exports = { module.exports = {
docs: [ docs: [
{
...
},
{ {
type: 'category', type: 'category',
label: 'Guides', label: 'Guides',
collapsed: false,
items: [ items: [
'guides/creating-pages', 'creating-pages',
{ {
Docs: ['docs-introduction', 'docs-sidebar', 'markdown-features', 'versioning'], type: 'category',
label: 'Docs',
items: ['introduction', 'sidebar', 'markdown-features', 'versioning'],
}, },
], ],
}, },
@ -242,7 +311,9 @@ module.exports = {
}; };
``` ```
**Note**: it's possible to use the shorthand syntax to create nested categories: :::tip
Use the **shorthand syntax** when you don't need **category options**:
```js title="sidebars.js" ```js title="sidebars.js"
module.exports = { module.exports = {
@ -250,28 +321,25 @@ module.exports = {
Guides: [ Guides: [
'creating-pages', 'creating-pages',
{ {
Docs: [ Docs: ['introduction', 'sidebar', 'markdown-features', 'versioning'],
'docs-introduction',
'docs-sidebar',
'markdown-features',
'versioning',
],
}, },
], ],
}, },
}; };
``` ```
:::
#### Collapsible categories {#collapsible-categories} #### Collapsible categories {#collapsible-categories}
For sites with a sizable amount of content, we support the option to expand/collapse a category to toggle the display of its contents. Categories are collapsible by default. If you want them to be always expanded, set `themeConfig.sidebarCollapsible` to `false`: For sites with a sizable amount of content, we support the option to expand/collapse a category to toggle the display of its contents. Categories are collapsible by default. If you want them to be always expanded, set `themeConfig.sidebarCollapsible` to `false`:
```js {4} title="docusaurus.config.js" ```js title="docusaurus.config.js"
module.exports = { module.exports = {
// ...
themeConfig: { themeConfig: {
// highlight-start
sidebarCollapsible: false, sidebarCollapsible: false,
// ... // highlight-end
}, },
}; };
``` ```
@ -296,16 +364,189 @@ module.exports = {
}; };
``` ```
### Autogenerated: generate a sidebar {#sidebar-item-autogenerated}
Docusaurus can **create a sidebar automatically** from your **filesystem structure**: each folder creates a sidebar category.
An `autogenerated` item is converted by Docusaurus to a **sidebar slice**: a list of items of type `doc` and `category`.
```typescript
type SidebarItemAutogenerated = {
type: 'autogenerated';
dirName: string; // Source folder to generate the sidebar slice from (relative to docs)
};
```
Docusaurus can generate a sidebar from your docs folder:
```js title="sidebars.js"
module.exports = {
myAutogeneratedSidebar: [
// highlight-start
{
type: 'autogenerated',
dirName: '.', // '.' means the current docs folder
},
// highlight-end
],
};
```
You can also use **multiple `autogenerated` items** in a sidebar, and interleave them with regular sidebar items:
```js title="sidebars.js"
module.exports = {
mySidebar: [
'intro',
{
type: 'category',
label: 'Tutorials',
items: [
'tutorial-intro',
// highlight-start
{
type: 'autogenerated',
dirName: 'tutorials/easy', // Generate sidebar slice from docs/tutorials/easy
},
// highlight-end
'tutorial-medium',
// highlight-start
{
type: 'autogenerated',
dirName: 'tutorials/advanced', // Generate sidebar slice from docs/tutorials/hard
},
// highlight-end
'tutorial-end',
],
},
// highlight-start
{
type: 'autogenerated',
dirName: 'guides', // Generate sidebar slice from docs/guides
},
// highlight-end
{
type: 'category',
label: 'Community',
items: ['team', 'chat'],
},
],
};
```
#### Autogenerated sidebar metadatas {#autogenerated-sidebar-metadatas}
By default, the sidebar slice will be generated in **alphabetical order** (using files and folders names).
If the generated sidebar does not look good, you can assign additional metadatas to docs and categories.
**For docs**: use additional frontmatter:
```diff title="docs/tutorials/tutorial-easy.md"
+ ---
+ sidebar_label: Easy
+ sidebar_position: 2
+ ---
# Easy Tutorial
This is the easy tutorial!
```
**For categories**: add a `_category_.json` or `_category_.yml` file in the appropriate folder:
```json title="docs/tutorials/_category_.json"
{
"label": "Tutorial",
"position": 3
}
```
```yaml title="docs/tutorials/_category_.yml"
label: 'Tutorial'
position: 2.5 # float position is supported
collapsed: false # keep the category open by default
```
:::info
The position metadata is only used **inside a sidebar slice**: Docusaurus does not re-order other items of your sidebar.
:::
#### Using number prefixes
A simple way to order an autogenerated sidebar is to prefix docs and folders by number prefixes:
```bash
docs
├── 01-Intro.md
├── 02-Tutorial Easy
│   ├── 01-First Part.md
│   ├── 02-Second Part.md
│   └── 03-End.md
├── 03-Tutorial Hard
│   ├── 01-First Part.md
│   ├── 02-Second Part.md
│   ├── 03-Third Part.md
│   └── 04-End.md
└── 04-End.md
```
To make it **easier to adopt**, Docusaurus supports **multiple number prefix patterns**.
By default, Docusaurus will **remove the number prefix** from the doc id, title, label and url paths.
:::caution
**Prefer using [additional metadatas](#autogenerated-sidebar-metadatas)**.
Updating a number prefix can be annoying, as it can require **updating multiple existing markdown links**:
```diff title="docs/02-Tutorial Easy/01-First Part.md"
- Check the [Tutorial End](../04-End.md);
+ Check the [Tutorial End](../05-End.md);
```
:::
#### Customize the sidebar items generator
You can provide a custom `sidebarItemsGenerator` function in the docs plugin (or preset) config:
```js title="docusaurus.config.js"
module.exports = {
plugins: [
[
'@docusaurus/plugin-content-docs',
{
/**
* Function used to replace the sidebar items of type "autogenerated"
* by real sidebar items (docs, categories, links...)
*/
// highlight-start
sidebarItemsGenerator: function ({item, version, docs}) {
// Use the provided data to create a custom "sidebar slice"
return [{type: 'doc', id: 'doc1'}];
},
// highlight-end
},
],
],
};
```
## Hideable sidebar {#hideable-sidebar} ## Hideable sidebar {#hideable-sidebar}
Using the enabled `themeConfig.hideableSidebar` option, you can make the entire sidebar hidden, allowing you to better focus your users on the content. This is especially useful when content consumption on medium screens (e.g. on tablets). Using the enabled `themeConfig.hideableSidebar` option, you can make the entire sidebar hidden, allowing you to better focus your users on the content. This is especially useful when content consumption on medium screens (e.g. on tablets).
```js {4} title="docusaurus.config.js" ```js title="docusaurus.config.js"
module.exports = { module.exports = {
// ...
themeConfig: { themeConfig: {
// highlight-starrt
hideableSidebar: true, hideableSidebar: true,
// ... // highlight-end
}, },
}; };
``` ```
@ -323,3 +564,21 @@ To pass in custom props to a swizzled sidebar item, add the optional `customProp
} }
} }
``` ```
## Complex sidebars example {#complex-sidebars-example}
Real-world example from the Docusaurus site:
```mdx-code-block
import CodeBlock from '@theme/CodeBlock';
<CodeBlock className="language-js" title="sidebars.js">
{require('!!raw-loader!@site/sidebars.js')
.default
.split('\n')
// remove comments
.map((line) => !['#','/*','*'].some(commentPattern => line.trim().startsWith(commentPattern)) && line)
.filter(Boolean)
.join('\n')}
</CodeBlock>
```

View file

@ -514,13 +514,15 @@ It is currently **not possible to link to a specific file** in Crowdin.
The **Docusaurus v2 configuration file** is a good example of using versioning and multi-instance: The **Docusaurus v2 configuration file** is a good example of using versioning and multi-instance:
```mdx-code-block
import CrowdinConfigV2 from '!!raw-loader!@site/../crowdin-v2.yaml'; import CrowdinConfigV2 from '!!raw-loader!@site/../crowdin-v2.yaml';
import CodeBlock from '@theme/CodeBlock'; import CodeBlock from '@theme/CodeBlock';
<CodeBlock className="language-yaml" title="test"> <CodeBlock className="language-yaml" title="crowdin.yml">
{CrowdinConfigV2.split('\n') {CrowdinConfigV2.split('\n')
// remove comments // remove comments
.map((line) => !line.startsWith('#') && line) .map((line) => !line.startsWith('#') && line)
.filter(Boolean) .filter(Boolean)
.join('\n')} .join('\n')}
</CodeBlock> </CodeBlock>
```

View file

@ -7,9 +7,10 @@
module.exports = { module.exports = {
community: [ community: [
'support', {
'team', type: 'autogenerated',
'resources', dirName: '.',
},
{ {
type: 'link', type: 'link',
href: '/showcase', href: '/showcase',

View file

@ -3480,6 +3480,11 @@
jest-diff "^26.0.0" jest-diff "^26.0.0"
pretty-format "^26.0.0" pretty-format "^26.0.0"
"@types/js-yaml@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.0.tgz#d1a11688112091f2c711674df3a65ea2f47b5dfb"
integrity sha512-4vlpCM5KPCL5CfGmTbpjwVKbISRYhduEJvvUWsH5EB7QInhEj94XPZ3ts/9FPiLZFqYO0xoW4ZL8z2AabTGgJA==
"@types/jscodeshift@^0.7.1": "@types/jscodeshift@^0.7.1":
version "0.7.1" version "0.7.1"
resolved "https://registry.yarnpkg.com/@types/jscodeshift/-/jscodeshift-0.7.1.tgz#8afcda6c8ca2ce828c3b192f8a1ba0245987ac12" resolved "https://registry.yarnpkg.com/@types/jscodeshift/-/jscodeshift-0.7.1.tgz#8afcda6c8ca2ce828c3b192f8a1ba0245987ac12"
@ -4504,6 +4509,11 @@ argparse@^1.0.10, argparse@^1.0.7:
dependencies: dependencies:
sprintf-js "~1.0.2" sprintf-js "~1.0.2"
argparse@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
aria-query@^4.2.2: aria-query@^4.2.2:
version "4.2.2" version "4.2.2"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b"
@ -6292,6 +6302,11 @@ columnify@^1.5.4:
strip-ansi "^3.0.0" strip-ansi "^3.0.0"
wcwidth "^1.0.0" wcwidth "^1.0.0"
combine-promises@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/combine-promises/-/combine-promises-1.1.0.tgz#72db90743c0ca7aab7d0d8d2052fd7b0f674de71"
integrity sha512-ZI9jvcLDxqwaXEixOhArm3r7ReIivsXkpbyEWyeOhzz1QS0iSgBPnWvEqvIQtYyamGCYA88gFhmUrs9hrrQ0pg==
combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
version "1.0.8" version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
@ -12017,6 +12032,13 @@ js-yaml@^3.11.0, js-yaml@^3.13.1, js-yaml@^3.14.0, js-yaml@^3.8.1:
argparse "^1.0.7" argparse "^1.0.7"
esprima "^4.0.0" esprima "^4.0.0"
js-yaml@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f"
integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==
dependencies:
argparse "^2.0.1"
jsbn@~0.1.0: jsbn@~0.1.0:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"