From e3fd3e74ce1bb87955cb87d2621611eb6cdb359b Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Fri, 4 Feb 2022 09:46:25 +0800 Subject: [PATCH] refactor(content-docs): clean up sidebars logic; validate generator returns (#6596) * refactor(content-docs): clean up sidebars logic; validate generator returns * remove another TODO * fix types * refactors * refactor... --- .../__snapshots__/index.test.ts.snap | 60 +++--- .../docusaurus-plugin-content-docs/src/cli.ts | 5 +- .../src/sidebars/README.md | 9 + .../sidebars-category-wrong-label.json | 11 - .../sidebars/sidebars-doc-id-not-string.json | 10 - .../sidebars/sidebars-link-wrong-href.json | 11 - .../sidebars/sidebars-link-wrong-label.json | 11 - .../sidebars/sidebars-unknown-type.json | 14 -- .../sidebars/sidebars-wrong-field.json | 20 -- .../__snapshots__/index.test.ts.snap | 32 ++- .../src/sidebars/__tests__/generator.test.ts | 23 +- .../src/sidebars/__tests__/index.test.ts | 164 +++------------ .../sidebars/__tests__/postProcessor.test.ts | 194 +++++++++++++++++ .../src/sidebars/__tests__/processor.test.ts | 106 +++------- .../src/sidebars/__tests__/validation.test.ts | 198 +++++++++++++++++- .../src/sidebars/generator.ts | 47 +++-- .../src/sidebars/index.ts | 46 ++-- .../src/sidebars/normalization.ts | 58 ++--- .../src/sidebars/postProcessor.ts | 94 +++++++++ .../src/sidebars/processor.ts | 116 +++------- .../src/sidebars/types.ts | 36 +++- .../src/sidebars/validation.ts | 88 ++++---- .../src/types.ts | 12 +- 23 files changed, 750 insertions(+), 615 deletions(-) create mode 100644 packages/docusaurus-plugin-content-docs/src/sidebars/README.md delete mode 100644 packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-category-wrong-label.json delete mode 100644 packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-doc-id-not-string.json delete mode 100644 packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-link-wrong-href.json delete mode 100644 packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-link-wrong-label.json delete mode 100644 packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-unknown-type.json delete mode 100644 packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-wrong-field.json create mode 100644 packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/postProcessor.test.ts create mode 100644 packages/docusaurus-plugin-content-docs/src/sidebars/postProcessor.ts diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap index 30b95bed8d..bba2e20e8e 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap @@ -770,8 +770,6 @@ Object { \\"docs\\": [ { \\"type\\": \\"category\\", - \\"collapsed\\": true, - \\"collapsible\\": true, \\"label\\": \\"Test\\", \\"items\\": [ { @@ -791,8 +789,8 @@ Object { \\"docId\\": \\"foo/baz\\" } ], - \\"collapsible\\": true, - \\"collapsed\\": true + \\"collapsed\\": true, + \\"collapsible\\": true }, { \\"type\\": \\"category\\", @@ -823,8 +821,8 @@ Object { \\"docId\\": \\"rootTryToEscapeSlug\\" } ], - \\"collapsible\\": true, \\"collapsed\\": true, + \\"collapsible\\": true, \\"href\\": \\"/docs/category/slugs\\" }, { @@ -844,12 +842,12 @@ Object { \\"href\\": \\"/docs/\\", \\"docId\\": \\"hello\\" } - ] + ], + \\"collapsed\\": true, + \\"collapsible\\": true }, { \\"type\\": \\"category\\", - \\"collapsed\\": true, - \\"collapsible\\": true, \\"label\\": \\"Guides\\", \\"items\\": [ { @@ -858,7 +856,9 @@ Object { \\"href\\": \\"/docs/\\", \\"docId\\": \\"hello\\" } - ] + ], + \\"collapsed\\": true, + \\"collapsible\\": true } ] }, @@ -3188,8 +3188,6 @@ Object { \\"version-1.0.0/docs\\": [ { \\"type\\": \\"category\\", - \\"collapsed\\": true, - \\"collapsible\\": true, \\"label\\": \\"Test\\", \\"items\\": [ { @@ -3204,12 +3202,12 @@ Object { \\"href\\": \\"/docs/1.0.0/foo/baz\\", \\"docId\\": \\"foo/baz\\" } - ] + ], + \\"collapsed\\": true, + \\"collapsible\\": true }, { \\"type\\": \\"category\\", - \\"collapsed\\": true, - \\"collapsible\\": true, \\"label\\": \\"Guides\\", \\"items\\": [ { @@ -3218,7 +3216,9 @@ Object { \\"href\\": \\"/docs/1.0.0/\\", \\"docId\\": \\"hello\\" } - ] + ], + \\"collapsed\\": true, + \\"collapsible\\": true } ] }, @@ -3255,8 +3255,6 @@ Object { \\"VersionedSideBarNameDoesNotMatter/docs\\": [ { \\"type\\": \\"category\\", - \\"collapsed\\": true, - \\"collapsible\\": true, \\"label\\": \\"Test\\", \\"items\\": [ { @@ -3265,12 +3263,12 @@ Object { \\"href\\": \\"/docs/foo/bar\\", \\"docId\\": \\"foo/bar\\" } - ] + ], + \\"collapsed\\": true, + \\"collapsible\\": true }, { \\"type\\": \\"category\\", - \\"collapsed\\": true, - \\"collapsible\\": true, \\"label\\": \\"Guides\\", \\"items\\": [ { @@ -3279,7 +3277,9 @@ Object { \\"href\\": \\"/docs/\\", \\"docId\\": \\"hello\\" } - ] + ], + \\"collapsed\\": true, + \\"collapsible\\": true } ] }, @@ -3310,8 +3310,6 @@ Object { \\"docs\\": [ { \\"type\\": \\"category\\", - \\"collapsed\\": true, - \\"collapsible\\": true, \\"label\\": \\"Test\\", \\"items\\": [ { @@ -3320,12 +3318,12 @@ Object { \\"href\\": \\"/docs/next/foo/barSlug\\", \\"docId\\": \\"foo/bar\\" } - ] + ], + \\"collapsed\\": true, + \\"collapsible\\": true }, { \\"type\\": \\"category\\", - \\"collapsed\\": true, - \\"collapsible\\": true, \\"label\\": \\"Guides\\", \\"items\\": [ { @@ -3334,7 +3332,9 @@ Object { \\"href\\": \\"/docs/next/\\", \\"docId\\": \\"hello\\" } - ] + ], + \\"collapsed\\": true, + \\"collapsible\\": true } ] }, @@ -3385,8 +3385,6 @@ Object { \\"version-1.0.1/docs\\": [ { \\"type\\": \\"category\\", - \\"collapsed\\": true, - \\"collapsible\\": true, \\"label\\": \\"Test\\", \\"items\\": [ { @@ -3395,7 +3393,9 @@ Object { \\"href\\": \\"/docs/withSlugs/rootAbsoluteSlug\\", \\"docId\\": \\"rootAbsoluteSlug\\" } - ] + ], + \\"collapsed\\": true, + \\"collapsible\\": true } ] }, diff --git a/packages/docusaurus-plugin-content-docs/src/cli.ts b/packages/docusaurus-plugin-content-docs/src/cli.ts index 6749eda5b1..a4cf9ee054 100644 --- a/packages/docusaurus-plugin-content-docs/src/cli.ts +++ b/packages/docusaurus-plugin-content-docs/src/cli.ts @@ -16,7 +16,7 @@ import type { PathOptions, SidebarOptions, } from '@docusaurus/plugin-content-docs'; -import {loadSidebarsFile, resolveSidebarPathOption} from './sidebars'; +import {loadSidebarsFileUnsafe, resolveSidebarPathOption} from './sidebars'; import {DEFAULT_PLUGIN_ID} from '@docusaurus/utils'; import logger from '@docusaurus/logger'; @@ -34,7 +34,8 @@ async function createVersionedSidebarFile({ // Load current sidebar and create a new versioned sidebars file (if needed). // Note: we don't need the sidebars file to be normalized: it's ok to let // plugin option changes to impact older, versioned sidebars - const sidebars = await loadSidebarsFile(sidebarPath); + // We don't validate here, assuming the user has already built the version + const sidebars = await loadSidebarsFileUnsafe(sidebarPath); // Do not create a useless versioned sidebars file if sidebars file is empty // or sidebars are disabled/false) diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/README.md b/packages/docusaurus-plugin-content-docs/src/sidebars/README.md new file mode 100644 index 0000000000..6b10e0602b --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/README.md @@ -0,0 +1,9 @@ +# Sidebars + +This part is very complicated and hard to navigate. Sidebars are loaded through the following steps: + +1. **Loading**. The sidebars file is read. Returns `SidebarsConfig`. +2. **Normalization**. The shorthands are expanded. This step is very lenient about the sidebars' shapes. Returns `NormalizedSidebars`. +3. **Validation**. The normalized sidebars are validated. This step happens after normalization, because the normalized sidebars are easier to validate, and allows us to repeatedly validate & generate in the future. +4. **Generation**. This step is done through the "processor" (naming is hard). The `autogenerated` items are unwrapped. In the future, steps 3 and 4 may be repeatedly done until all autogenerated items are unwrapped. Returns `ProcessedSidebars`. +5. **Post-processing**. Defaults are applied (collapsed states), category links are resolved, empty categories are flattened. Returns `Sidebars`. diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-category-wrong-label.json b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-category-wrong-label.json deleted file mode 100644 index d86e00db5d..0000000000 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-category-wrong-label.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "docs": { - "Test": [ - { - "type": "category", - "label": true, - "items": ["doc1"] - } - ] - } -} diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-doc-id-not-string.json b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-doc-id-not-string.json deleted file mode 100644 index e778910ad1..0000000000 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-doc-id-not-string.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "docs": { - "Test": [ - { - "type": "doc", - "id": ["doc1"] - } - ] - } -} diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-link-wrong-href.json b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-link-wrong-href.json deleted file mode 100644 index 6a13b92b83..0000000000 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-link-wrong-href.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "docs": { - "Test": [ - { - "type": "link", - "label": "GitHub", - "href": ["example.com"] - } - ] - } -} diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-link-wrong-label.json b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-link-wrong-label.json deleted file mode 100644 index 39dcab0749..0000000000 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-link-wrong-label.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "docs": { - "Test": [ - { - "type": "link", - "label": false, - "href": "https://github.com" - } - ] - } -} diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-unknown-type.json b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-unknown-type.json deleted file mode 100644 index 618c52d817..0000000000 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-unknown-type.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "docs": { - "Test": [ - "foo/bar", - "foo/baz", - { - "type": "superman" - } - ], - "Guides": [ - "hello" - ] - } -} diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-wrong-field.json b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-wrong-field.json deleted file mode 100644 index 43dff88d7c..0000000000 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-wrong-field.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "docs": { - "Test": [ - "foo/bar", - "foo/baz", - { - "type": "category", - "label": "category", - "href": "https://github.com" - }, - { - "type": "ref", - "id": "hello" - } - ], - "Guides": [ - "hello" - ] - } -} diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__snapshots__/index.test.ts.snap index be3e4e8800..ed1a6911de 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__snapshots__/index.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`loadNormalizedSidebars sidebars link 1`] = ` +exports[`loadSidebars sidebars link 1`] = ` Object { "docs": Array [ Object { @@ -21,7 +21,7 @@ Object { } `; -exports[`loadNormalizedSidebars sidebars with category.collapsed property 1`] = ` +exports[`loadSidebars sidebars with category.collapsed property 1`] = ` Object { "docs": Array [ Object { @@ -72,7 +72,7 @@ Object { } `; -exports[`loadNormalizedSidebars sidebars with category.collapsed property at first level 1`] = ` +exports[`loadSidebars sidebars with category.collapsed property at first level 1`] = ` Object { "docs": Array [ Object { @@ -105,7 +105,7 @@ Object { } `; -exports[`loadNormalizedSidebars sidebars with deep level of category 1`] = ` +exports[`loadSidebars sidebars with deep level of category 1`] = ` Object { "docs": Array [ Object { @@ -177,7 +177,7 @@ Object { } `; -exports[`loadNormalizedSidebars sidebars with first level not a category 1`] = ` +exports[`loadSidebars sidebars with first level not a category 1`] = ` Object { "docs": Array [ Object { @@ -201,7 +201,7 @@ Object { } `; -exports[`loadNormalizedSidebars sidebars with known sidebar item type 1`] = ` +exports[`loadSidebars sidebars with known sidebar item type 1`] = ` Object { "docs": Array [ Object { @@ -246,3 +246,23 @@ Object { ], } `; + +exports[`loadSidebars undefined path 1`] = ` +Object { + "defaultSidebar": Array [ + Object { + "collapsed": true, + "collapsible": true, + "items": Array [ + Object { + "id": "bar", + "type": "doc", + }, + ], + "label": "foo", + "link": undefined, + "type": "category", + }, + ], +} +`; diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/generator.test.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/generator.test.ts index 296f145cc6..dfda57ad80 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/generator.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/generator.test.ts @@ -214,8 +214,6 @@ describe('DefaultSidebarItemsGenerator', () => { { type: 'category', label: 'Tutorials', - collapsed: true, - collapsible: true, link: { type: 'doc', id: 'tutorials-index', @@ -228,19 +226,16 @@ describe('DefaultSidebarItemsGenerator', () => { { type: 'category', label: 'Guides', - collapsed: false, - collapsible: true, link: { type: 'doc', id: 'guides-index', }, + collapsed: false, items: [ {type: 'doc', id: 'guide1', className: 'foo'}, { type: 'category', label: 'SubGuides (metadata file label)', - collapsed: true, - collapsible: true, items: [{type: 'doc', id: 'nested-guide'}], link: { type: 'generated-index', @@ -279,8 +274,6 @@ describe('DefaultSidebarItemsGenerator', () => { 'subfolder/subsubfolder/subsubsubfolder3': { position: 1, label: 'subsubsubfolder3 (_category_.json label)', - collapsible: false, - collapsed: false, link: { type: 'doc', id: 'doc1', // This is a "fully-qualified" ID that can't be found locally @@ -355,8 +348,6 @@ describe('DefaultSidebarItemsGenerator', () => { { type: 'category', label: 'subsubsubfolder3 (_category_.json label)', - collapsed: false, - collapsible: false, link: { id: 'doc1', type: 'doc', @@ -369,8 +360,6 @@ describe('DefaultSidebarItemsGenerator', () => { { type: 'category', label: 'subsubsubfolder2 (_category_.yml label)', - collapsed: true, - collapsible: true, className: 'bar', items: [{type: 'doc', id: 'doc6'}], }, @@ -379,8 +368,6 @@ describe('DefaultSidebarItemsGenerator', () => { { type: 'category', label: 'subsubsubfolder', - collapsed: true, - collapsible: true, items: [{type: 'doc', id: 'doc5'}], }, ] as Sidebar); @@ -458,8 +445,6 @@ describe('DefaultSidebarItemsGenerator', () => { { type: 'category', label: 'Category label', - collapsed: true, - collapsible: true, link: { id: 'parent/doc3', type: 'doc', @@ -478,8 +463,6 @@ describe('DefaultSidebarItemsGenerator', () => { { type: 'category', label: 'Category 2 label', - collapsed: true, - collapsible: true, items: [ { id: 'parent/doc4', @@ -583,8 +566,6 @@ describe('DefaultSidebarItemsGenerator', () => { { type: 'category', label: 'Tutorials', - collapsed: true, - collapsible: true, link: { type: 'doc', id: 'tutorials-index', @@ -597,8 +578,6 @@ describe('DefaultSidebarItemsGenerator', () => { { type: 'category', label: 'Guides', - collapsed: true, - collapsible: true, items: [ {type: 'doc', id: 'guide1', className: 'foo'}, {type: 'doc', id: 'guide2'}, diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/index.test.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/index.test.ts index 20a2f44ccb..96e18a9e86 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/index.test.ts @@ -6,32 +6,36 @@ */ import path from 'path'; -import { - loadNormalizedSidebars, - DefaultSidebars, - DisabledSidebars, -} from '../index'; -import type {NormalizeSidebarsParams, VersionMetadata} from '../../types'; +import {loadSidebars, DisabledSidebars} from '../index'; +import type {SidebarProcessorParams} from '../types'; +import {DefaultSidebarItemsGenerator} from '../generator'; -describe('loadNormalizedSidebars', () => { +describe('loadSidebars', () => { const fixtureDir = path.join(__dirname, '__fixtures__', 'sidebars'); - const options: NormalizeSidebarsParams = { - sidebarCollapsed: true, - sidebarCollapsible: true, - version: { - versionName: 'version', - versionPath: 'versionPath', - } as VersionMetadata, + const params: SidebarProcessorParams = { + sidebarItemsGenerator: DefaultSidebarItemsGenerator, + numberPrefixParser: (filename) => ({filename}), + docs: [ + { + source: '@site/docs/foo/bar.md', + sourceDirName: 'foo', + id: 'bar', + frontMatter: {}, + }, + ], + version: {contentPath: 'docs/foo', contentPathLocalized: 'docs/foo'}, + categoryLabelSlugger: null, + sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: true}, }; test('sidebars with known sidebar item type', async () => { const sidebarPath = path.join(fixtureDir, 'sidebars.json'); - const result = await loadNormalizedSidebars(sidebarPath, options); + const result = await loadSidebars(sidebarPath, params); expect(result).toMatchSnapshot(); }); test('sidebars with deep level of category', async () => { const sidebarPath = path.join(fixtureDir, 'sidebars-category.js'); - const result = await loadNormalizedSidebars(sidebarPath, options); + const result = await loadSidebars(sidebarPath, params); expect(result).toMatchSnapshot(); }); @@ -41,8 +45,8 @@ describe('loadNormalizedSidebars', () => { fixtureDir, 'sidebars-category-shorthand.js', ); - const sidebar1 = await loadNormalizedSidebars(sidebarPath1, options); - const sidebar2 = await loadNormalizedSidebars(sidebarPath2, options); + const sidebar1 = await loadSidebars(sidebarPath1, params); + const sidebar2 = await loadSidebars(sidebarPath2, params); expect(sidebar1).toEqual(sidebar2); }); @@ -51,53 +55,11 @@ describe('loadNormalizedSidebars', () => { fixtureDir, 'sidebars-category-wrong-items.json', ); - await expect(() => loadNormalizedSidebars(sidebarPath, options)).rejects - .toThrowErrorMatchingInlineSnapshot(` - "{ - \\"type\\": \\"category\\", - \\"label\\": \\"Category Label\\", - \\"items\\" [1]: \\"doc1\\" - } -  - [1] \\"items\\" must be an array" - `); - }); - - test('sidebars with category but category label is not a string', async () => { - const sidebarPath = path.join( - fixtureDir, - 'sidebars-category-wrong-label.json', + await expect(() => + loadSidebars(sidebarPath, params), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Invalid category {\\"type\\":\\"category\\",\\"label\\":\\"Category Label\\",\\"items\\":\\"doc1\\"}: items must be an array"`, ); - await expect(() => loadNormalizedSidebars(sidebarPath, options)).rejects - .toThrowErrorMatchingInlineSnapshot(` - "{ - \\"type\\": \\"category\\", - \\"items\\": [ - \\"doc1\\" - ], - \\"label\\" [1]: true - } -  - [1] \\"label\\" must be a string" - `); - }); - - test('sidebars item doc but id is not a string', async () => { - const sidebarPath = path.join( - fixtureDir, - 'sidebars-doc-id-not-string.json', - ); - await expect(() => loadNormalizedSidebars(sidebarPath, options)).rejects - .toThrowErrorMatchingInlineSnapshot(` - "{ - \\"type\\": \\"doc\\", - \\"id\\" [1]: [ - \\"doc1\\" - ] - } -  - [1] \\"id\\" must be a string" - `); }); test('sidebars with first level not a category', async () => { @@ -105,95 +67,35 @@ describe('loadNormalizedSidebars', () => { fixtureDir, 'sidebars-first-level-not-category.js', ); - const result = await loadNormalizedSidebars(sidebarPath, options); + const result = await loadSidebars(sidebarPath, params); expect(result).toMatchSnapshot(); }); test('sidebars link', async () => { const sidebarPath = path.join(fixtureDir, 'sidebars-link.json'); - const result = await loadNormalizedSidebars(sidebarPath, options); + const result = await loadSidebars(sidebarPath, params); expect(result).toMatchSnapshot(); }); - test('sidebars link wrong label', async () => { - const sidebarPath = path.join(fixtureDir, 'sidebars-link-wrong-label.json'); - await expect(() => loadNormalizedSidebars(sidebarPath, options)).rejects - .toThrowErrorMatchingInlineSnapshot(` - "{ - \\"type\\": \\"link\\", - \\"href\\": \\"https://github.com\\", - \\"label\\" [1]: false - } -  - [1] \\"label\\" must be a string" - `); - }); - - test('sidebars link wrong href', async () => { - const sidebarPath = path.join(fixtureDir, 'sidebars-link-wrong-href.json'); - await expect(() => loadNormalizedSidebars(sidebarPath, options)).rejects - .toThrowErrorMatchingInlineSnapshot(` - "{ - \\"type\\": \\"link\\", - \\"label\\": \\"GitHub\\", - \\"href\\" [1]: [ - \\"example.com\\" - ] - } -  - [1] \\"href\\" contains an invalid value" - `); - }); - - test('sidebars with unknown sidebar item type', async () => { - const sidebarPath = path.join(fixtureDir, 'sidebars-unknown-type.json'); - await expect(() => loadNormalizedSidebars(sidebarPath, options)).rejects - .toThrowErrorMatchingInlineSnapshot(` - "{ - \\"type\\": \\"superman\\", - \\"undefined\\" [1]: -- missing -- - } -  - [1] Unknown sidebar item type \\"superman\\"." - `); - }); - - test('sidebars with known sidebar item type but wrong field', async () => { - const sidebarPath = path.join(fixtureDir, 'sidebars-wrong-field.json'); - await expect(() => loadNormalizedSidebars(sidebarPath, options)).rejects - .toThrowErrorMatchingInlineSnapshot(` - "{ - \\"type\\": \\"category\\", - \\"label\\": \\"category\\", - \\"href\\": \\"https://github.com\\", - \\"items\\" [1]: -- missing -- - } -  - [1] \\"items\\" is required" - `); - }); - test('unexisting path', async () => { - await expect(loadNormalizedSidebars('badpath', options)).resolves.toEqual( + await expect(loadSidebars('badpath', params)).resolves.toEqual( DisabledSidebars, ); }); test('undefined path', async () => { - await expect(loadNormalizedSidebars(undefined, options)).resolves.toEqual( - DefaultSidebars, - ); + await expect(loadSidebars(undefined, params)).resolves.toMatchSnapshot(); }); test('literal false path', async () => { - await expect(loadNormalizedSidebars(false, options)).resolves.toEqual( + await expect(loadSidebars(false, params)).resolves.toEqual( DisabledSidebars, ); }); test('sidebars with category.collapsed property', async () => { const sidebarPath = path.join(fixtureDir, 'sidebars-collapsed.json'); - const result = await loadNormalizedSidebars(sidebarPath, options); + const result = await loadSidebars(sidebarPath, params); expect(result).toMatchSnapshot(); }); @@ -202,7 +104,7 @@ describe('loadNormalizedSidebars', () => { fixtureDir, 'sidebars-collapsed-first-level.json', ); - const result = await loadNormalizedSidebars(sidebarPath, options); + const result = await loadSidebars(sidebarPath, params); expect(result).toMatchSnapshot(); }); }); diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/postProcessor.test.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/postProcessor.test.ts new file mode 100644 index 0000000000..699c69ae6a --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/postProcessor.test.ts @@ -0,0 +1,194 @@ +/** + * 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 {postProcessSidebars} from '../postProcessor'; + +describe('postProcess', () => { + test('transforms category without subitems', () => { + const processedSidebar = postProcessSidebars( + { + sidebar: [ + { + type: 'category', + label: 'Category', + link: { + type: 'generated-index', + slug: 'generated/permalink', + }, + items: [], + }, + { + type: 'category', + label: 'Category 2', + link: { + type: 'doc', + id: 'doc ID', + }, + items: [], + }, + ], + }, + { + sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: true}, + version: {versionPath: 'version'}, + }, + ); + + expect(processedSidebar).toMatchInlineSnapshot(` + Object { + "sidebar": Array [ + Object { + "href": "version/generated/permalink", + "label": "Category", + "type": "link", + }, + Object { + "id": "doc ID", + "label": "Category 2", + "type": "doc", + }, + ], + } + `); + + expect(() => { + postProcessSidebars( + { + sidebar: [ + { + type: 'category', + label: 'Bad category', + items: [], + }, + ], + }, + { + sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: true}, + version: {versionPath: 'version'}, + }, + ); + }).toThrowErrorMatchingInlineSnapshot( + `"Sidebar category Bad category has neither any subitem nor a link. This makes this item not able to link to anything."`, + ); + }); + + test('corrects collapsed state inconsistencies', () => { + expect( + postProcessSidebars( + { + sidebar: [ + { + type: 'category', + label: 'Category', + collapsed: true, + collapsible: false, + items: [{type: 'doc', id: 'foo'}], + }, + ], + }, + + { + sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: true}, + version: {versionPath: 'version'}, + }, + ), + ).toMatchInlineSnapshot(` + Object { + "sidebar": Array [ + Object { + "collapsed": false, + "collapsible": false, + "items": Array [ + Object { + "id": "foo", + "type": "doc", + }, + ], + "label": "Category", + "link": undefined, + "type": "category", + }, + ], + } + `); + + expect( + postProcessSidebars( + { + sidebar: [ + { + type: 'category', + label: 'Category', + collapsed: true, + items: [{type: 'doc', id: 'foo'}], + }, + ], + }, + + { + sidebarOptions: {sidebarCollapsed: false, sidebarCollapsible: false}, + version: {versionPath: 'version'}, + }, + ), + ).toMatchInlineSnapshot(` + Object { + "sidebar": Array [ + Object { + "collapsed": false, + "collapsible": false, + "items": Array [ + Object { + "id": "foo", + "type": "doc", + }, + ], + "label": "Category", + "link": undefined, + "type": "category", + }, + ], + } + `); + + expect( + postProcessSidebars( + { + sidebar: [ + { + type: 'category', + label: 'Category', + items: [{type: 'doc', id: 'foo'}], + }, + ], + }, + + { + sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: false}, + version: {versionPath: 'version'}, + }, + ), + ).toMatchInlineSnapshot(` + Object { + "sidebar": Array [ + Object { + "collapsed": false, + "collapsible": false, + "items": Array [ + Object { + "id": "foo", + "type": "doc", + }, + ], + "label": "Category", + "link": undefined, + "type": "category", + }, + ], + } + `); + }); +}); diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/processor.test.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/processor.test.ts index 31ea227594..68d65a2d3a 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/processor.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/processor.test.ts @@ -5,12 +5,15 @@ * LICENSE file in the root directory of this source tree. */ -import {processSidebars, type SidebarProcessorParams} from '../processor'; +import {processSidebars} from '../processor'; import type { SidebarItem, SidebarItemsGenerator, - Sidebars, + NormalizedSidebar, NormalizedSidebars, + SidebarProcessorParams, + CategoryMetadataFile, + ProcessedSidebars, } from '../types'; import {DefaultSidebarItemsGenerator} from '../generator'; import {createSlugger} from '@docusaurus/utils'; @@ -25,7 +28,7 @@ describe('processSidebars', () => { return jest.fn(async () => sidebarSlice); } - const StaticGeneratedSidebarSlice: SidebarItem[] = [ + const StaticGeneratedSidebarSlice: NormalizedSidebar = [ {type: 'doc', id: 'doc-generated-id-1'}, {type: 'doc', id: 'doc-generated-id-2'}, ]; @@ -53,9 +56,10 @@ describe('processSidebars', () => { async function testProcessSidebars( unprocessedSidebars: NormalizedSidebars, + categoriesMetadata: Record = {}, paramsOverrides: Partial = {}, ) { - return processSidebars(unprocessedSidebars, { + return processSidebars(unprocessedSidebars, categoriesMetadata, { ...params, ...paramsOverrides, }); @@ -101,10 +105,7 @@ describe('processSidebars', () => { link: { type: 'generated-index', slug: 'category-generated-index-slug', - permalink: 'category-generated-index-permalink', }, - collapsed: true, // A suspicious bad config that will be normalized - collapsible: false, items: [ {type: 'doc', id: 'doc2'}, {type: 'autogenerated', dirName: 'dir1'}, @@ -131,6 +132,7 @@ describe('processSidebars', () => { expect(StaticSidebarItemsGenerator).toHaveBeenCalledTimes(3); expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({ + categoriesMetadata: {}, defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator, item: {type: 'autogenerated', dirName: 'dir1'}, docs: params.docs, @@ -143,6 +145,7 @@ describe('processSidebars', () => { }); expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({ defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator, + categoriesMetadata: {}, item: {type: 'autogenerated', dirName: 'dir2'}, docs: params.docs, version: { @@ -154,6 +157,7 @@ describe('processSidebars', () => { }); expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({ defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator, + categoriesMetadata: {}, item: {type: 'autogenerated', dirName: 'dir3'}, docs: params.docs, version: { @@ -173,10 +177,7 @@ describe('processSidebars', () => { link: { type: 'generated-index', slug: 'category-generated-index-slug', - permalink: 'category-generated-index-permalink', }, - collapsed: false, - collapsible: false, items: [{type: 'doc', id: 'doc2'}, ...StaticGeneratedSidebarSlice], }, {type: 'link', href: 'https://facebook.com', label: 'FB'}, @@ -194,20 +195,17 @@ describe('processSidebars', () => { items: [{type: 'doc', id: 'doc4'}], }, ], - } as Sidebars); + } as ProcessedSidebars); }); test('ensure generated items are normalized', async () => { - const sidebarSliceContainingCategoryGeneratedIndex: SidebarItem[] = [ + const sidebarSliceContainingCategoryGeneratedIndex: NormalizedSidebar = [ { type: 'category', label: 'Generated category', link: { type: 'generated-index', slug: 'generated-cat-index-slug', - // @ts-expect-error: TODO undefined should be allowed here, - // typing error needing refactor - permalink: undefined, }, items: [ { @@ -218,15 +216,19 @@ describe('processSidebars', () => { }, ]; - const unprocessedSidebars: NormalizedSidebars = { + const unprocessedSidebars = { someSidebar: [{type: 'autogenerated', dirName: 'dir2'}], }; - const processedSidebar = await testProcessSidebars(unprocessedSidebars, { - sidebarItemsGenerator: createStaticSidebarItemGenerator( - sidebarSliceContainingCategoryGeneratedIndex, - ), - }); + const processedSidebar = await testProcessSidebars( + unprocessedSidebars, + {}, + { + sidebarItemsGenerator: createStaticSidebarItemGenerator( + sidebarSliceContainingCategoryGeneratedIndex, + ), + }, + ); expect(processedSidebar).toEqual({ someSidebar: [ @@ -236,7 +238,6 @@ describe('processSidebars', () => { link: { type: 'generated-index', slug: 'generated-cat-index-slug', - permalink: '/docs/1.0.0/generated-cat-index-slug', }, items: [ { @@ -244,67 +245,8 @@ describe('processSidebars', () => { id: 'foo', }, ], - collapsible: true, - collapsed: true, }, ], - } as Sidebars); - }); - - test('transforms category without subitems', async () => { - const sidebarSlice: SidebarItem[] = [ - { - type: 'category', - label: 'Category', - link: { - type: 'generated-index', - permalink: 'generated/permalink', - }, - items: [], - }, - { - type: 'category', - label: 'Category 2', - link: { - type: 'doc', - id: 'doc ID', - }, - items: [], - }, - ]; - - const processedSidebar = await testProcessSidebars( - {sidebar: sidebarSlice}, - {}, - ); - - expect(processedSidebar).toEqual({ - sidebar: [ - { - type: 'link', - label: 'Category', - href: 'generated/permalink', - }, - { - type: 'doc', - label: 'Category 2', - id: 'doc ID', - }, - ], - } as Sidebars); - - await expect(async () => { - await testProcessSidebars({ - sidebar: [ - { - type: 'category', - label: 'Bad category', - items: [], - }, - ], - }); - }).rejects.toThrowErrorMatchingInlineSnapshot( - `"Sidebar category Bad category has neither any subitem nor a link. This makes this item not able to link to anything."`, - ); + } as ProcessedSidebars); }); }); diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/validation.test.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/validation.test.ts index 28a0d0eb19..2088302876 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/validation.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/validation.test.ts @@ -6,14 +6,9 @@ */ import {validateSidebars, validateCategoryMetadataFile} from '../validation'; -import type {CategoryMetadataFile} from '../generator'; -import type {SidebarsConfig} from '../types'; +import type {SidebarsConfig, CategoryMetadataFile} from '../types'; describe('validateSidebars', () => { - // TODO add more tests - - // TODO it seems many error cases are not validated properly - // and error messages are quite bad test('throw for bad value', async () => { expect(() => validateSidebars({sidebar: [{type: 42}]})) .toThrowErrorMatchingInlineSnapshot(` @@ -45,10 +40,193 @@ describe('validateSidebars', () => { }; validateSidebars(sidebars); }); -}); -describe('html item type', () => { - test('requires a value', () => { + test('sidebar category wrong label', () => { + expect(() => + validateSidebars({ + docs: [ + { + type: 'category', + label: true, + items: [{type: 'doc', id: 'doc1'}], + }, + ], + }), + ).toThrowErrorMatchingInlineSnapshot(` + "{ + \\"type\\": \\"category\\", + \\"items\\": [ + { + \\"type\\": \\"doc\\", + \\"id\\": \\"doc1\\" + } + ], + \\"label\\" [1]: true + } +  + [1] \\"label\\" must be a string" + `); + }); + + test('sidebars link wrong label', () => { + expect(() => + validateSidebars({ + docs: [ + { + type: 'link', + label: false, + href: 'https://github.com', + }, + ], + }), + ).toThrowErrorMatchingInlineSnapshot(` + "{ + \\"type\\": \\"link\\", + \\"href\\": \\"https://github.com\\", + \\"label\\" [1]: false + } +  + [1] \\"label\\" must be a string" + `); + }); + + test('sidebars link wrong href', () => { + expect(() => + validateSidebars({ + docs: [ + { + type: 'link', + label: 'GitHub', + href: ['example.com'], + }, + ], + }), + ).toThrowErrorMatchingInlineSnapshot(` + "{ + \\"type\\": \\"link\\", + \\"label\\": \\"GitHub\\", + \\"href\\" [1]: [ + \\"example.com\\" + ] + } +  + [1] \\"href\\" contains an invalid value" + `); + }); + + test('sidebars with unknown sidebar item type', () => { + expect(() => + validateSidebars({ + docs: [ + { + type: 'superman', + }, + ], + }), + ).toThrowErrorMatchingInlineSnapshot(` + "{ + \\"type\\": \\"superman\\", + \\"undefined\\" [1]: -- missing -- + } +  + [1] Unknown sidebar item type \\"superman\\"." + `); + }); + + test('sidebars category missing items', () => { + expect(() => + validateSidebars({ + docs: [ + { + type: 'category', + label: 'category', + }, + + { + type: 'ref', + id: 'hello', + }, + ], + }), + ).toThrowErrorMatchingInlineSnapshot(` + "{ + \\"type\\": \\"category\\", + \\"label\\": \\"category\\", + \\"items\\" [1]: -- missing -- + } +  + [1] \\"items\\" is required" + `); + }); + + test('sidebars category wrong field', () => { + expect(() => + validateSidebars({ + docs: [ + { + type: 'category', + label: 'category', + items: [], + href: 'https://google.com', + }, + + { + type: 'ref', + id: 'hello', + }, + ], + }), + ).toThrowErrorMatchingInlineSnapshot(` + "{ + \\"type\\": \\"category\\", + \\"label\\": \\"category\\", + \\"items\\": [], + \\"href\\" [1]: \\"https://google.com\\" + } +  + [1] \\"href\\" is not allowed" + `); + }); + + test('sidebar category wrong items', () => { + expect(() => + validateSidebars({ + docs: { + Test: [ + { + type: 'category', + label: 'Category Label', + items: 'doc1', + }, + ], + }, + }), + ).toThrowErrorMatchingInlineSnapshot(`"sidebar.forEach is not a function"`); + }); + + test('sidebars item doc but id is not a string', async () => { + expect(() => + validateSidebars({ + docs: [ + { + type: 'doc', + id: ['doc1'], + }, + ], + }), + ).toThrowErrorMatchingInlineSnapshot(` + "{ + \\"type\\": \\"doc\\", + \\"id\\" [1]: [ + \\"doc1\\" + ] + } +  + [1] \\"id\\" must be a string" + `); + }); + + test('HTML type requires a value', () => { const sidebars: SidebarsConfig = { sidebar1: [ { @@ -68,7 +246,7 @@ describe('html item type', () => { `); }); - test('accepts valid values', () => { + test('HTML type accepts valid values', () => { const sidebars: SidebarsConfig = { sidebar1: [ { diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts index fd7a0c1a03..03239e3ec4 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts @@ -6,12 +6,12 @@ */ import type { - SidebarItem, SidebarItemDoc, - SidebarItemCategory, SidebarItemsGenerator, SidebarItemsGeneratorDoc, - SidebarItemCategoryLink, + NormalizedSidebarItemCategory, + NormalizedSidebarItem, + SidebarItemCategoryLinkConfig, } from './types'; import {sortBy, last} from 'lodash'; import {addTrailingSlash, posixPath} from '@docusaurus/utils'; @@ -48,7 +48,6 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ numberPrefixParser, isCategoryIndex, docs: allDocs, - options, item: {dirName: autogenDir}, categoriesMetadata, }) => { @@ -125,7 +124,9 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ * Step 3. Recursively transform the tree-like structure to sidebar items. * (From a record to an array of items, akin to normalizing shorthand) */ - function generateSidebar(fsModel: Dir): Promise[]> { + function generateSidebar( + fsModel: Dir, + ): Promise[]> { function createDocItem(id: string): WithPosition { const { sidebarPosition: position, @@ -145,7 +146,7 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ dir: Dir, fullPath: string, folderName: string, - ): Promise> { + ): Promise> { const categoryMetadata = categoriesMetadata[posixPath(path.join(autogenDir, fullPath))]; const className = categoryMetadata?.className; @@ -160,18 +161,19 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ // using the "local id" (myDoc) or "qualified id" (dirName/myDoc) function findDocByLocalId(localId: string): SidebarItemDoc | undefined { return allItems.find( - (item) => item.type === 'doc' && getLocalDocId(item.id) === localId, - ) as SidebarItemDoc | undefined; + (item): item is SidebarItemDoc => + item.type === 'doc' && getLocalDocId(item.id) === localId, + ); } function findConventionalCategoryDocLink(): SidebarItemDoc | undefined { - return allItems.find((item) => { + return allItems.find((item): item is SidebarItemDoc => { if (item.type !== 'doc') { return false; } const doc = getDoc(item.id); return isCategoryIndex(toCategoryIndexMatcherParam(doc)); - }) as SidebarItemDoc | undefined; + }); } function getCategoryLinkedDocId(): string | undefined { @@ -190,13 +192,13 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ const categoryLinkedDocId = getCategoryLinkedDocId(); - const link: SidebarItemCategoryLink | undefined = categoryLinkedDocId - ? { - type: 'doc', - id: categoryLinkedDocId, // We "remap" a potentially "local id" to a "qualified id" - } - : // TODO typing issue - (categoryMetadata?.link as SidebarItemCategoryLink | undefined); + const link: SidebarItemCategoryLinkConfig | null | undefined = + categoryLinkedDocId + ? { + type: 'doc', + id: categoryLinkedDocId, // We "remap" a potentially "local id" to a "qualified id" + } + : categoryMetadata?.link; // If a doc is linked, remove it from the category subItems const items = allItems.filter( @@ -206,9 +208,8 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ return { type: 'category', label: categoryMetadata?.label ?? filename, - collapsible: - categoryMetadata?.collapsible ?? options.sidebarCollapsible, - collapsed: categoryMetadata?.collapsed ?? options.sidebarCollapsed, + collapsible: categoryMetadata?.collapsible, + collapsed: categoryMetadata?.collapsed, position: categoryMetadata?.position ?? numberPrefix, ...(className !== undefined && {className}), items, @@ -219,7 +220,7 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ dir: Dir | null, // The directory item to be transformed. itemKey: string, // For docs, it's the doc ID; for categories, it's used to generate the next `relativePath`. fullPath: string, // `dir`'s full path relative to the autogen dir. - ): Promise> { + ): Promise> { return dir ? createCategoryItem(dir, fullPath, itemKey) : createDocItem(itemKey.substring(docIdPrefix.length)); @@ -238,7 +239,9 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ * consecutive sidebar slices (i.e. a whole category composed of multiple * autogenerated items) */ - function sortItems(sidebarItems: WithPosition[]): SidebarItem[] { + function sortItems( + sidebarItems: WithPosition[], + ): NormalizedSidebarItem[] { const processedSidebarItems = sidebarItems.map((item) => { if (item.type === 'category') { return {...item, items: sortItems(item.items)}; diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/index.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/index.ts index ba8172a75d..2d095b2096 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/index.ts @@ -7,13 +7,13 @@ import fs from 'fs-extra'; import importFresh from 'import-fresh'; -import type {SidebarsConfig, Sidebars, NormalizedSidebars} from './types'; -import type {NormalizeSidebarsParams} from '../types'; +import type {SidebarsConfig, Sidebars, SidebarProcessorParams} from './types'; import {validateSidebars, validateCategoryMetadataFile} from './validation'; import {normalizeSidebars} from './normalization'; -import {processSidebars, type SidebarProcessorParams} from './processor'; +import {processSidebars} from './processor'; +import {postProcessSidebars} from './postProcessor'; import path from 'path'; -import {createSlugger, Globby} from '@docusaurus/utils'; +import {Globby} from '@docusaurus/utils'; import logger from '@docusaurus/logger'; import type {PluginOptions} from '@docusaurus/plugin-content-docs'; import Yaml from 'js-yaml'; @@ -69,7 +69,7 @@ async function readCategoriesMetadata(contentPath: string) { ); } -async function loadSidebarsFileUnsafe( +export async function loadSidebarsFileUnsafe( sidebarFilePath: string | false | undefined, ): Promise { // false => no sidebars @@ -93,37 +93,21 @@ async function loadSidebarsFileUnsafe( return importFresh(sidebarFilePath); } -export async function loadSidebarsFile( - sidebarFilePath: string | false | undefined, -): Promise { - const sidebarsConfig = await loadSidebarsFileUnsafe(sidebarFilePath); - validateSidebars(sidebarsConfig); - return sidebarsConfig; -} - -export async function loadNormalizedSidebars( - sidebarFilePath: string | false | undefined, - params: NormalizeSidebarsParams, -): Promise { - return normalizeSidebars(await loadSidebarsFile(sidebarFilePath), params); -} - // Note: sidebarFilePath must be absolute, use resolveSidebarPathOption export async function loadSidebars( sidebarFilePath: string | false | undefined, - options: Omit, + options: SidebarProcessorParams, ): Promise { - const normalizeSidebarsParams: NormalizeSidebarsParams = { - ...options.sidebarOptions, - version: options.version, - categoryLabelSlugger: createSlugger(), - }; - const normalizedSidebars = await loadNormalizedSidebars( - sidebarFilePath, - normalizeSidebarsParams, - ); + const sidebarsConfig = await loadSidebarsFileUnsafe(sidebarFilePath); + const normalizedSidebars = normalizeSidebars(sidebarsConfig); + validateSidebars(normalizedSidebars); const categoriesMetadata = await readCategoriesMetadata( options.version.contentPath, ); - return processSidebars(normalizedSidebars, {...options, categoriesMetadata}); + const processedSidebars = await processSidebars( + normalizedSidebars, + categoriesMetadata, + options, + ); + return postProcessSidebars(processedSidebars, options); } diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/normalization.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/normalization.ts index 48666e8987..6fa04a8102 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/normalization.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/normalization.ts @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -import type {NormalizeSidebarsParams} from '../types'; import type { NormalizedSidebarItem, NormalizedSidebar, @@ -15,41 +14,16 @@ import type { SidebarItemConfig, SidebarConfig, SidebarsConfig, - SidebarItemCategoryLink, NormalizedSidebarItemCategory, } from './types'; import {isCategoriesShorthand} from './utils'; import {mapValues} from 'lodash'; -import {normalizeUrl} from '@docusaurus/utils'; -import type {SidebarOptions} from '@docusaurus/plugin-content-docs'; - -function normalizeCategoryLink( - category: SidebarItemCategoryConfig, - params: NormalizeSidebarsParams, -): SidebarItemCategoryLink | undefined { - if (category.link?.type === 'generated-index') { - // default slug logic can be improved - const getDefaultSlug = () => - `/category/${params.categoryLabelSlugger.slug(category.label)}`; - const slug = category.link.slug ?? getDefaultSlug(); - const permalink = normalizeUrl([params.version.versionPath, slug]); - return { - ...category.link, - slug, - permalink, - }; - } - return category.link; -} function normalizeCategoriesShorthand( sidebar: SidebarCategoriesShorthand, - options: SidebarOptions, ): SidebarItemCategoryConfig[] { return Object.entries(sidebar).map(([label, items]) => ({ type: 'category', - collapsed: options.sidebarCollapsed, - collapsible: options.sidebarCollapsible, label, items, })); @@ -61,7 +35,6 @@ function normalizeCategoriesShorthand( */ export function normalizeItem( item: SidebarItemConfig, - options: NormalizeSidebarsParams, ): NormalizedSidebarItem[] { if (typeof item === 'string') { return [ @@ -72,42 +45,35 @@ export function normalizeItem( ]; } if (isCategoriesShorthand(item)) { - return normalizeCategoriesShorthand(item, options).flatMap((subItem) => - normalizeItem(subItem, options), + return normalizeCategoriesShorthand(item).flatMap((subItem) => + normalizeItem(subItem), ); } if (item.type === 'category') { - const link = normalizeCategoryLink(item, options); + if (typeof item.items !== 'undefined' && !Array.isArray(item.items)) { + throw new Error( + `Invalid category ${JSON.stringify(item)}: items must be an array`, + ); + } const normalizedCategory: NormalizedSidebarItemCategory = { ...item, - link, - items: (item.items ?? []).flatMap((subItem) => - normalizeItem(subItem, options), - ), - collapsible: item.collapsible ?? options.sidebarCollapsible, - collapsed: item.collapsed ?? options.sidebarCollapsed, + items: item.items.flatMap((subItem) => normalizeItem(subItem)), }; return [normalizedCategory]; } return [item]; } -function normalizeSidebar( - sidebar: SidebarConfig, - options: NormalizeSidebarsParams, -): NormalizedSidebar { +function normalizeSidebar(sidebar: SidebarConfig): NormalizedSidebar { const normalizedSidebar = Array.isArray(sidebar) ? sidebar - : normalizeCategoriesShorthand(sidebar, options); + : normalizeCategoriesShorthand(sidebar); - return normalizedSidebar.flatMap((subItem) => - normalizeItem(subItem, options), - ); + return normalizedSidebar.flatMap((subItem) => normalizeItem(subItem)); } export function normalizeSidebars( sidebars: SidebarsConfig, - params: NormalizeSidebarsParams, ): NormalizedSidebars { - return mapValues(sidebars, (items) => normalizeSidebar(items, params)); + return mapValues(sidebars, normalizeSidebar); } diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/postProcessor.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/postProcessor.ts new file mode 100644 index 0000000000..7a30354ebd --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/postProcessor.ts @@ -0,0 +1,94 @@ +/** + * 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 {normalizeUrl} from '@docusaurus/utils'; +import type { + SidebarItem, + Sidebars, + SidebarProcessorParams, + ProcessedSidebarItemCategory, + ProcessedSidebarItem, + ProcessedSidebars, + SidebarItemCategoryLink, +} from './types'; +import {mapValues} from 'lodash'; + +function normalizeCategoryLink( + category: ProcessedSidebarItemCategory, + params: SidebarProcessorParams, +): SidebarItemCategoryLink | undefined { + if (category.link?.type === 'generated-index') { + // default slug logic can be improved + const getDefaultSlug = () => + `/category/${params.categoryLabelSlugger.slug(category.label)}`; + const slug = category.link.slug ?? getDefaultSlug(); + const permalink = normalizeUrl([params.version.versionPath, slug]); + return { + ...category.link, + slug, + permalink, + }; + } + return category.link; +} + +function postProcessSidebarItem( + item: ProcessedSidebarItem, + params: SidebarProcessorParams, +): SidebarItem { + if (item.type === 'category') { + const category = { + ...item, + collapsed: item.collapsed ?? params.sidebarOptions.sidebarCollapsed, + collapsible: item.collapsible ?? params.sidebarOptions.sidebarCollapsible, + link: normalizeCategoryLink(item, params), + items: item.items.map((subItem) => + postProcessSidebarItem(subItem, params), + ), + }; + // If the current category doesn't have subitems, we render a normal link + // instead. + if (category.items.length === 0) { + if (!category.link) { + throw new Error( + `Sidebar category ${item.label} has neither any subitem nor a link. This makes this item not able to link to anything.`, + ); + } + switch (category.link.type) { + case 'doc': + return { + type: 'doc', + label: category.label, + id: category.link.id, + }; + case 'generated-index': + return { + type: 'link', + label: category.label, + href: category.link.permalink, + }; + default: + throw new Error('Unexpected sidebar category link type'); + } + } + // A non-collapsible category can't be collapsed! + if (category.collapsible === false) { + category.collapsed = false; + } + return category; + } + return item; +} + +export function postProcessSidebars( + sidebars: ProcessedSidebars, + params: SidebarProcessorParams, +): Sidebars { + return mapValues(sidebars, (sidebar) => + sidebar.map((item) => postProcessSidebarItem(item, params)), + ); +} diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/processor.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/processor.ts index f1c860715b..f7432222e4 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/processor.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/processor.ts @@ -7,41 +7,23 @@ import type {DocMetadataBase, VersionMetadata} from '../types'; import type { - Sidebars, - Sidebar, - SidebarItem, NormalizedSidebarItem, NormalizedSidebar, NormalizedSidebars, - SidebarItemsGeneratorOption, SidebarItemsGeneratorDoc, SidebarItemsGeneratorVersion, - NormalizedSidebarItemCategory, - SidebarItemCategory, SidebarItemAutogenerated, + ProcessedSidebarItem, + ProcessedSidebar, + ProcessedSidebars, + SidebarProcessorParams, CategoryMetadataFile, } from './types'; -import {transformSidebarItems} from './utils'; import {DefaultSidebarItemsGenerator} from './generator'; +import {validateSidebars} from './validation'; import {mapValues, memoize, pick} from 'lodash'; import combinePromises from 'combine-promises'; -import {normalizeItem} from './normalization'; import {isCategoryIndex} from '../docs'; -import type {Slugger} from '@docusaurus/utils'; -import type { - NumberPrefixParser, - SidebarOptions, -} from '@docusaurus/plugin-content-docs'; - -export type SidebarProcessorParams = { - sidebarItemsGenerator: SidebarItemsGeneratorOption; - numberPrefixParser: NumberPrefixParser; - docs: DocMetadataBase[]; - version: VersionMetadata; - categoryLabelSlugger: Slugger; - sidebarOptions: SidebarOptions; - categoriesMetadata: Record; -}; function toSidebarItemsGeneratorDoc( doc: DocMetadataBase, @@ -66,15 +48,15 @@ function toSidebarItemsGeneratorVersion( // post-processing checks async function processSidebar( unprocessedSidebar: NormalizedSidebar, + categoriesMetadata: Record, params: SidebarProcessorParams, -): Promise { +): Promise { const { sidebarItemsGenerator, numberPrefixParser, docs, version, sidebarOptions, - categoriesMetadata, } = params; // Just a minor lazy transformation optimization @@ -83,20 +65,9 @@ async function processSidebar( version: toSidebarItemsGeneratorVersion(version), })); - async function processCategoryItem( - item: NormalizedSidebarItemCategory, - ): Promise { - return { - ...item, - items: (await Promise.all(item.items.map(processItem))).flat(), - }; - } - async function processAutoGeneratedItem( item: SidebarItemAutogenerated, - ): Promise { - // TODO the returned type can't be trusted in practice (generator can be - // user-provided) + ): Promise { const generatedItems = await sidebarItemsGenerator({ item, numberPrefixParser, @@ -106,50 +77,23 @@ async function processSidebar( options: sidebarOptions, categoriesMetadata, }); - // TODO validate generated items: user can generate bad items - - const generatedItemsNormalized = generatedItems.flatMap((generatedItem) => - normalizeItem(generatedItem, {...params, ...sidebarOptions}), - ); - // Process again... weird but sidebar item generated might generate some // auto-generated items? - return processItems(generatedItemsNormalized); + // TODO repeatedly process & unwrap autogenerated items until there are no + // more autogenerated items, or when loop count (e.g. 10) is reached + return processItems(generatedItems); } async function processItem( item: NormalizedSidebarItem, - ): Promise { + ): Promise { if (item.type === 'category') { - // If the current category doesn't have subitems, we render a normal doc link instead. - if (item.items.length === 0) { - if (!item.link) { - throw new Error( - `Sidebar category ${item.label} has neither any subitem nor a link. This makes this item not able to link to anything.`, - ); - } - switch (item.link.type) { - case 'doc': - return [ - { - type: 'doc', - label: item.label, - id: item.link.id, - }, - ]; - case 'generated-index': - return [ - { - type: 'link', - label: item.label, - href: item.link.permalink, - }, - ]; - default: - throw new Error('Unexpected sidebar category link type'); - } - } - return [await processCategoryItem(item)]; + return [ + { + ...item, + items: (await Promise.all(item.items.map(processItem))).flat(), + }, + ]; } if (item.type === 'autogenerated') { return processAutoGeneratedItem(item); @@ -159,32 +103,24 @@ async function processSidebar( async function processItems( items: NormalizedSidebarItem[], - ): Promise { + ): Promise { return (await Promise.all(items.map(processItem))).flat(); } const processedSidebar = await processItems(unprocessedSidebar); - - const fixSidebarItemInconsistencies = (item: SidebarItem): SidebarItem => { - // A non-collapsible category can't be collapsed! - if (item.type === 'category' && !item.collapsible && item.collapsed) { - return { - ...item, - collapsed: false, - }; - } - return item; - }; - return transformSidebarItems(processedSidebar, fixSidebarItemInconsistencies); + return processedSidebar; } export async function processSidebars( unprocessedSidebars: NormalizedSidebars, + categoriesMetadata: Record, params: SidebarProcessorParams, -): Promise { - return combinePromises( +): Promise { + const processedSidebars = await combinePromises( mapValues(unprocessedSidebars, (unprocessedSidebar) => - processSidebar(unprocessedSidebar, params), + processSidebar(unprocessedSidebar, categoriesMetadata, params), ), ); + validateSidebars(processedSidebars); + return processedSidebars; } diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/types.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/types.ts index face305336..7a3d4fb5a5 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/types.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/types.ts @@ -12,6 +12,7 @@ import type { SidebarOptions, CategoryIndexMatcher, } from '@docusaurus/plugin-content-docs'; +import type {Slugger} from '@docusaurus/utils'; // Makes all properties visible when hovering over the type type Expand> = {[P in keyof T]: T[P]}; @@ -107,9 +108,9 @@ export type SidebarsConfig = { // Normalized but still has 'autogenerated', which will be handled in processing export type NormalizedSidebarItemCategory = Expand< - SidebarItemCategoryBase & { + Optional & { items: NormalizedSidebarItem[]; - link?: SidebarItemCategoryLink; + link?: SidebarItemCategoryLinkConfig; } >; @@ -125,6 +126,22 @@ export type NormalizedSidebars = { [sidebarId: string]: NormalizedSidebar; }; +export type ProcessedSidebarItemCategory = Expand< + Optional & { + items: ProcessedSidebarItem[]; + link?: SidebarItemCategoryLinkConfig; + } +>; +export type ProcessedSidebarItem = + | SidebarItemDoc + | SidebarItemHtml + | SidebarItemLink + | ProcessedSidebarItemCategory; +export type ProcessedSidebar = ProcessedSidebarItem[]; +export type ProcessedSidebars = { + [sidebarId: string]: ProcessedSidebar; +}; + export type SidebarItemCategory = Expand< SidebarItemCategoryBase & { items: SidebarItem[]; @@ -230,9 +247,7 @@ export type SidebarItemsGeneratorArgs = { }; export type SidebarItemsGenerator = ( generatorArgs: SidebarItemsGeneratorArgs, -) => // TODO TS issue: the generator can generate un-normalized items! -Promise; -// Promise; +) => Promise; // Also inject the default generator to conveniently wrap/enhance/sort the // default sidebar gen logic @@ -242,4 +257,13 @@ export type SidebarItemsGeneratorOptionArgs = { } & SidebarItemsGeneratorArgs; export type SidebarItemsGeneratorOption = ( generatorArgs: SidebarItemsGeneratorOptionArgs, -) => Promise; +) => Promise; + +export type SidebarProcessorParams = { + sidebarItemsGenerator: SidebarItemsGeneratorOption; + numberPrefixParser: NumberPrefixParser; + docs: DocMetadataBase[]; + version: VersionMetadata; + categoryLabelSlugger: Slugger; + sidebarOptions: SidebarOptions; +}; diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts index 4a11c0d8d2..76dfb7ec18 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts @@ -8,7 +8,6 @@ import {Joi, URISchema} from '@docusaurus/utils-validation'; import type { SidebarItemConfig, - SidebarCategoriesShorthand, SidebarItemBase, SidebarItemAutogenerated, SidebarItemDoc, @@ -16,12 +15,13 @@ import type { SidebarItemLink, SidebarItemCategoryConfig, SidebarItemCategoryLink, - SidebarsConfig, SidebarItemCategoryLinkDoc, SidebarItemCategoryLinkGeneratedIndex, + NormalizedSidebars, + NormalizedSidebarItem, + NormalizedSidebarItemCategory, CategoryMetadataFile, } from './types'; -import {isCategoriesShorthand} from './utils'; // NOTE: we don't add any default values during validation on purpose! // Config types are exposed to users for typechecking and we use the same type @@ -52,7 +52,7 @@ const sidebarItemDocSchema = sidebarItemBaseSchema.append({ const sidebarItemHtmlSchema = sidebarItemBaseSchema.append({ type: 'html', value: Joi.string().required(), - defaultStyle: Joi.boolean().default(false), + defaultStyle: Joi.boolean(), }); const sidebarItemLinkSchema = sidebarItemBaseSchema.append({ @@ -88,14 +88,13 @@ const sidebarItemCategoryLinkSchema = Joi.object() }), }, { - is: Joi.string().required(), + is: Joi.required(), then: Joi.forbidden().messages({ 'any.unknown': 'Unknown sidebar category link type "{.type}".', }), }, ], - }) - .id('sidebarCategoryLinkSchema'); + }); const sidebarItemCategorySchema = sidebarItemBaseSchema.append({ @@ -103,10 +102,11 @@ const sidebarItemCategorySchema = label: Joi.string() .required() .messages({'any.unknown': '"label" must be a string'}), - // TODO: Joi doesn't allow mutual recursion. See https://github.com/sideway/joi/issues/2611 items: Joi.array() .required() - .messages({'any.unknown': '"items" must be an array'}), // .items(Joi.link('#sidebarItemSchema')), + .messages({'any.unknown': '"items" must be an array'}), + // TODO: Joi doesn't allow mutual recursion. See https://github.com/sideway/joi/issues/2611 + // .items(Joi.link('#sidebarItemSchema')), link: sidebarItemCategoryLinkSchema, collapsed: Joi.boolean().messages({ 'any.unknown': '"collapsed" must be a boolean', @@ -116,56 +116,44 @@ const sidebarItemCategorySchema = }), }); -const sidebarItemSchema: Joi.Schema = Joi.object() - .when('.type', { - switch: [ - {is: 'link', then: sidebarItemLinkSchema}, - { - is: Joi.string().valid('doc', 'ref').required(), - then: sidebarItemDocSchema, - }, - {is: 'html', then: sidebarItemHtmlSchema}, - {is: 'autogenerated', then: sidebarItemAutogeneratedSchema}, - {is: 'category', then: sidebarItemCategorySchema}, - { - is: Joi.any().required(), - then: Joi.forbidden().messages({ - 'any.unknown': 'Unknown sidebar item type "{.type}".', - }), - }, - ], - }) - .id('sidebarItemSchema'); +const sidebarItemSchema = Joi.object().when('.type', { + switch: [ + {is: 'link', then: sidebarItemLinkSchema}, + { + is: Joi.string().valid('doc', 'ref').required(), + then: sidebarItemDocSchema, + }, + {is: 'html', then: sidebarItemHtmlSchema}, + {is: 'autogenerated', then: sidebarItemAutogeneratedSchema}, + {is: 'category', then: sidebarItemCategorySchema}, + { + is: Joi.any().required(), + then: Joi.forbidden().messages({ + 'any.unknown': 'Unknown sidebar item type "{.type}".', + }), + }, + ], +}); +// .id('sidebarItemSchema'); -function validateSidebarItem(item: unknown): asserts item is SidebarItemConfig { - if (typeof item === 'string') { - return; - } +function validateSidebarItem( + item: unknown, +): asserts item is NormalizedSidebarItem { // TODO: remove once with proper Joi support // Because we can't use Joi to validate nested items (see above), we do it // manually - if (isCategoriesShorthand(item as SidebarItemConfig)) { - Object.values(item as SidebarCategoriesShorthand).forEach((category) => - category.forEach(validateSidebarItem), - ); - } else { - Joi.assert(item, sidebarItemSchema); + Joi.assert(item, sidebarItemSchema); - if ((item as SidebarItemCategoryConfig).type === 'category') { - (item as SidebarItemCategoryConfig).items.forEach(validateSidebarItem); - } + if ((item as NormalizedSidebarItemCategory).type === 'category') { + (item as NormalizedSidebarItemCategory).items.forEach(validateSidebarItem); } } export function validateSidebars( - sidebars: unknown, -): asserts sidebars is SidebarsConfig { - Object.values(sidebars as SidebarsConfig).forEach((sidebar) => { - if (Array.isArray(sidebar)) { - sidebar.forEach(validateSidebarItem); - } else { - validateSidebarItem(sidebar); - } + sidebars: Record, +): asserts sidebars is NormalizedSidebars { + Object.values(sidebars as NormalizedSidebars).forEach((sidebar) => { + sidebar.forEach(validateSidebarItem); }); } diff --git a/packages/docusaurus-plugin-content-docs/src/types.ts b/packages/docusaurus-plugin-content-docs/src/types.ts index b32575772f..e4a96f1079 100644 --- a/packages/docusaurus-plugin-content-docs/src/types.ts +++ b/packages/docusaurus-plugin-content-docs/src/types.ts @@ -8,15 +8,12 @@ /// import type {Sidebars} from './sidebars/types'; -import type {Tag, FrontMatterTag, Slugger} from '@docusaurus/utils'; +import type {Tag, FrontMatterTag} from '@docusaurus/utils'; import type { BrokenMarkdownLink as IBrokenMarkdownLink, ContentPaths, } from '@docusaurus/utils/lib/markdownLinks'; -import type { - VersionBanner, - SidebarOptions, -} from '@docusaurus/plugin-content-docs'; +import type {VersionBanner} from '@docusaurus/plugin-content-docs'; export type DocFile = { contentPath: string; // /!\ may be localized @@ -41,11 +38,6 @@ export type VersionMetadata = ContentPaths & { routePriority: number | undefined; // -1 for the latest docs }; -export type NormalizeSidebarsParams = SidebarOptions & { - version: VersionMetadata; - categoryLabelSlugger: Slugger; -}; - export type LastUpdateData = { lastUpdatedAt?: number; formattedLastUpdatedAt?: string;