diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/README.md b/packages/docusaurus-plugin-content-docs/src/sidebars/README.md index 6b10e0602bd0..0f1f26e55e77 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/README.md +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/README.md @@ -6,4 +6,5 @@ This part is very complicated and hard to navigate. Sidebars are loaded through 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`. + - **Important**: this step should only care about unwrapping autogenerated items, not filtering them, writing additional metadata, applying defaults, etc.—everything will be handled in the post-processor. Important because the generator is exposed to the end-user and we want it to be easy to be reasoned about. 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-index.json b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-category-index.json new file mode 100644 index 000000000000..9e54b3d19e52 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-category-index.json @@ -0,0 +1,23 @@ +{ + "docs": [ + { + "label": "Tutorials", + "type": "category", + "items": [ + { + "type": "autogenerated", + "dirName": "tutorials" + } + ] + }, + { + "label": "index-only", + "type": "category", + "link": { + "type": "doc", + "id": "tutorials/tutorial-basics" + }, + "items": [] + } + ] +} diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-drafts.json b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-drafts.json new file mode 100644 index 000000000000..52aedd8fd62b --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__fixtures__/sidebars/sidebars-drafts.json @@ -0,0 +1,60 @@ +{ + "sidebar": [ + "draft1", + { + "type": "category", + "label": "all drafts", + "items": [ + "draft2", + "draft3" + ] + }, + { + "type": "category", + "label": "all drafts", + "link": { + "type": "generated-index" + }, + "items": [ + "draft2", + "draft3" + ] + }, + { + "type": "category", + "label": "all drafts", + "link": { + "type": "doc", + "id": "draft1" + }, + "items": [ + "draft2", + "draft3" + ] + }, + { + "type": "category", + "label": "index not draft", + "link": { + "type": "doc", + "id": "not-draft" + }, + "items": [ + "draft2", + "draft3" + ] + }, + { + "type": "category", + "label": "subitem not draft", + "link": { + "type": "doc", + "id": "draft1" + }, + "items": [ + "not-draft", + "draft3" + ] + } + ] +} 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 f5b2acdd5190..608ffc753814 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,5 +1,56 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`loadSidebars loads sidebars with index-only categories 1`] = ` +{ + "docs": [ + { + "collapsed": true, + "collapsible": true, + "items": [ + { + "id": "tutorials/tutorial-basics", + "label": "tutorial-basics", + "type": "doc", + }, + ], + "label": "Tutorials", + "link": undefined, + "type": "category", + }, + { + "id": "tutorials/tutorial-basics", + "label": "index-only", + "type": "doc", + }, + ], +} +`; + +exports[`loadSidebars loads sidebars with interspersed draft items 1`] = ` +{ + "sidebar": [ + { + "id": "not-draft", + "label": "index not draft", + "type": "doc", + }, + { + "collapsed": true, + "collapsible": true, + "items": [ + { + "id": "not-draft", + "type": "doc", + }, + ], + "label": "subitem not draft", + "link": undefined, + "type": "category", + }, + ], +} +`; + exports[`loadSidebars sidebars link 1`] = ` { "docs": [ diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__snapshots__/postProcessor.test.ts.snap b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__snapshots__/postProcessor.test.ts.snap index cb42628261f3..3284571628ec 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__snapshots__/postProcessor.test.ts.snap +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__snapshots__/postProcessor.test.ts.snap @@ -60,14 +60,21 @@ exports[`postProcess corrects collapsed state inconsistencies 3`] = ` } `; -exports[`postProcess transforms category without subitems 1`] = ` +exports[`postProcess filters draft items 1`] = ` { "sidebar": [ { - "href": "version/generated/permalink", + "id": "another", "label": "Category", - "type": "link", + "type": "doc", }, + ], +} +`; + +exports[`postProcess transforms category without subitems 1`] = ` +{ + "sidebar": [ { "id": "doc ID", "label": "Category 2", 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 3a92144ffd8a..aedcfe0db332 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 @@ -7,6 +7,7 @@ import {jest} from '@jest/globals'; import path from 'path'; +import {createSlugger} from '@docusaurus/utils'; import {loadSidebars, DisabledSidebars} from '../index'; import type {SidebarProcessorParams} from '../types'; import {DefaultSidebarItemsGenerator} from '../generator'; @@ -27,6 +28,7 @@ describe('loadSidebars', () => { ], drafts: [], version: { + path: 'version', contentPath: path.join(fixtureDir, 'docs'), contentPathLocalized: path.join(fixtureDir, 'docs'), }, @@ -124,6 +126,32 @@ describe('loadSidebars', () => { expect(result).toMatchSnapshot(); }); + it('loads sidebars with index-only categories', async () => { + const sidebarPath = path.join(fixtureDir, 'sidebars-category-index.json'); + const result = await loadSidebars(sidebarPath, { + ...params, + docs: [ + { + id: 'tutorials/tutorial-basics', + source: '@site/docs/tutorials/tutorial-basics/index.md', + sourceDirName: 'tutorials/tutorial-basics', + frontMatter: {}, + }, + ], + }); + expect(result).toMatchSnapshot(); + }); + + it('loads sidebars with interspersed draft items', async () => { + const sidebarPath = path.join(fixtureDir, 'sidebars-drafts.json'); + const result = await loadSidebars(sidebarPath, { + ...params, + drafts: [{id: 'draft1'}, {id: 'draft2'}, {id: 'draft3'}], + categoryLabelSlugger: createSlugger(), + }); + expect(result).toMatchSnapshot(); + }); + it('duplicate category metadata files', async () => { const sidebarPath = path.join( fixtureDir, 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 index a1a8e56d8fd1..16196c7c3de8 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/postProcessor.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/postProcessor.test.ts @@ -35,6 +35,7 @@ describe('postProcess', () => { { sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: true}, version: {path: 'version'}, + drafts: [], }, ); @@ -54,6 +55,7 @@ describe('postProcess', () => { { sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: true}, version: {path: 'version'}, + drafts: [], }, ); }).toThrowErrorMatchingInlineSnapshot( @@ -79,6 +81,7 @@ describe('postProcess', () => { { sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: true}, version: {path: 'version'}, + drafts: [], }, ), ).toMatchSnapshot(); @@ -99,6 +102,7 @@ describe('postProcess', () => { { sidebarOptions: {sidebarCollapsed: false, sidebarCollapsible: false}, version: {path: 'version'}, + drafts: [], }, ), ).toMatchSnapshot(); @@ -118,6 +122,37 @@ describe('postProcess', () => { { sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: false}, version: {path: 'version'}, + drafts: [], + }, + ), + ).toMatchSnapshot(); + }); + + it('filters draft items', () => { + expect( + postProcessSidebars( + { + sidebar: [ + { + type: 'category', + label: 'Category', + items: [{type: 'doc', id: 'foo'}], + }, + { + type: 'category', + label: 'Category', + link: { + type: 'doc', + id: 'another', + }, + items: [{type: 'doc', id: 'foo'}], + }, + ], + }, + { + sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: true}, + version: {path: 'version'}, + drafts: [{id: 'foo', unversionedId: 'foo'}], }, ), ).toMatchSnapshot(); diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/postProcessor.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/postProcessor.ts index ac662291d343..717db0860583 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/postProcessor.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/postProcessor.ts @@ -15,12 +15,20 @@ import type { ProcessedSidebars, SidebarItemCategoryLink, } from './types'; +import {getDocIds} from '../docs'; import _ from 'lodash'; +type SidebarPostProcessorParams = SidebarProcessorParams & { + draftIds: Set; +}; + function normalizeCategoryLink( category: ProcessedSidebarItemCategory, - params: SidebarProcessorParams, + params: SidebarPostProcessorParams, ): SidebarItemCategoryLink | undefined { + if (category.link?.type === 'doc' && params.draftIds.has(category.link.id)) { + return undefined; + } if (category.link?.type === 'generated-index') { // Default slug logic can be improved const getDefaultSlug = () => @@ -38,37 +46,42 @@ function normalizeCategoryLink( function postProcessSidebarItem( item: ProcessedSidebarItem, - params: SidebarProcessorParams, -): SidebarItem { + params: SidebarPostProcessorParams, +): SidebarItem | null { if (item.type === 'category') { + // Fail-fast if there's actually no subitems, no because all subitems are + // drafts. This is likely a configuration mistake. + if (item.items.length === 0 && !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.`, + ); + } 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), - ), + items: item.items + .map((subItem) => postProcessSidebarItem(subItem, params)) + .filter((v): v is SidebarItem => Boolean(v)), }; // 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.`, - ); + // Doesn't make sense to render an empty generated index page, so we + // filter the entire category out as well. + if ( + !category.link || + category.link.type === 'generated-index' || + params.draftIds.has(category.link.id) + ) { + return null; } - return category.link.type === 'doc' - ? { - type: 'doc', - label: category.label, - id: category.link.id, - } - : { - type: 'link', - label: category.label, - href: category.link.permalink, - }; + return { + type: 'doc', + label: category.label, + id: category.link.id, + }; } // A non-collapsible category can't be collapsed! if (category.collapsible === false) { @@ -76,6 +89,12 @@ function postProcessSidebarItem( } return category; } + if ( + (item.type === 'doc' || item.type === 'ref') && + params.draftIds.has(item.id) + ) { + return null; + } return item; } @@ -83,7 +102,11 @@ export function postProcessSidebars( sidebars: ProcessedSidebars, params: SidebarProcessorParams, ): Sidebars { + const draftIds = new Set(params.drafts.flatMap(getDocIds)); + return _.mapValues(sidebars, (sidebar) => - sidebar.map((item) => postProcessSidebarItem(item, params)), + sidebar + .map((item) => postProcessSidebarItem(item, {...params, draftIds})) + .filter((v): v is SidebarItem => Boolean(v)), ); } diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/processor.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/processor.ts index b0153e9dcbbc..7e83dd08a921 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/processor.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/processor.ts @@ -26,7 +26,7 @@ import {DefaultSidebarItemsGenerator} from './generator'; import {validateSidebars} from './validation'; import _ from 'lodash'; import combinePromises from 'combine-promises'; -import {getDocIds, isCategoryIndex} from '../docs'; +import {isCategoryIndex} from '../docs'; function toSidebarItemsGeneratorDoc( doc: DocMetadataBase, @@ -55,8 +55,7 @@ async function processSidebar( categoriesMetadata: {[filePath: string]: CategoryMetadataFile}, params: SidebarProcessorParams, ): Promise { - const {sidebarItemsGenerator, numberPrefixParser, docs, drafts, version} = - params; + const {sidebarItemsGenerator, numberPrefixParser, docs, version} = params; // Just a minor lazy transformation optimization const getSidebarItemsGeneratorDocsAndVersion = _.memoize(() => ({ @@ -82,19 +81,6 @@ async function processSidebar( return processItems(generatedItems); } - const draftIds = new Set(drafts.flatMap(getDocIds)); - - const isDraftItem = (item: NormalizedSidebarItem): boolean => { - if (item.type === 'doc' || item.type === 'ref') { - return draftIds.has(item.id); - } - // If a category only contains draft items, it should be filtered entirely. - if (item.type === 'category') { - return item.items.every(isDraftItem); - } - return false; - }; - async function processItem( item: NormalizedSidebarItem, ): Promise { @@ -102,7 +88,7 @@ async function processSidebar( return [ { ...item, - items: await processItems(item.items), + items: (await Promise.all(item.items.map(processItem))).flat(), }, ]; } @@ -115,9 +101,7 @@ async function processSidebar( async function processItems( items: NormalizedSidebarItem[], ): Promise { - return ( - await Promise.all(items.filter((i) => !isDraftItem(i)).map(processItem)) - ).flat(); + return (await Promise.all(items.map(processItem))).flat(); } const processedSidebar = await processItems(unprocessedSidebar); diff --git a/website/community/Versions.tsx b/website/community/4-canary/Versions.tsx similarity index 100% rename from website/community/Versions.tsx rename to website/community/4-canary/Versions.tsx diff --git a/website/community/4-canary.md b/website/community/4-canary/index.md similarity index 100% rename from website/community/4-canary.md rename to website/community/4-canary/index.md diff --git a/website/docs/api/misc/img/logger-demo.png b/website/docs/api/misc/logger/demo.png similarity index 100% rename from website/docs/api/misc/img/logger-demo.png rename to website/docs/api/misc/logger/demo.png diff --git a/website/docs/api/misc/logger.md b/website/docs/api/misc/logger/logger.md similarity index 99% rename from website/docs/api/misc/logger.md rename to website/docs/api/misc/logger/logger.md index 986c4d2880da..fb291473dd5f 100644 --- a/website/docs/api/misc/logger.md +++ b/website/docs/api/misc/logger/logger.md @@ -64,4 +64,4 @@ An embedded expression is optionally preceded by a flag in the form `[a-z]+=` (a If the expression is an array, it's formatted by `` `\n- ${array.join('\n- ')}\n` `` (note it automatically gets a leading line end). Each member is formatted by itself and the bullet is not formatted. So you would see the above message printed as: -![demo](./img/logger-demo.png) +![demo](./demo.png)