mirror of
https://github.com/facebook/docusaurus.git
synced 2025-08-03 16:59:06 +02:00
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:
parent
836f92708a
commit
db79d462ab
67 changed files with 2887 additions and 306 deletions
|
@ -16,9 +16,18 @@ import {
|
|||
Sidebar,
|
||||
SidebarItemCategory,
|
||||
SidebarItemType,
|
||||
UnprocessedSidebarItem,
|
||||
UnprocessedSidebars,
|
||||
UnprocessedSidebar,
|
||||
DocMetadataBase,
|
||||
VersionMetadata,
|
||||
SidebarItemsGenerator,
|
||||
SidebarItemsGeneratorDoc,
|
||||
SidebarItemsGeneratorVersion,
|
||||
} from './types';
|
||||
import {mapValues, flatten, flatMap, difference} from 'lodash';
|
||||
import {mapValues, flatten, flatMap, difference, pick, memoize} from 'lodash';
|
||||
import {getElementsAround} from '@docusaurus/utils';
|
||||
import combinePromises from 'combine-promises';
|
||||
|
||||
type SidebarItemCategoryJSON = SidebarItemBase & {
|
||||
type: 'category';
|
||||
|
@ -27,12 +36,18 @@ type SidebarItemCategoryJSON = SidebarItemBase & {
|
|||
collapsed?: boolean;
|
||||
};
|
||||
|
||||
type SidebarItemAutogeneratedJSON = SidebarItemBase & {
|
||||
type: 'autogenerated';
|
||||
dirName: string;
|
||||
};
|
||||
|
||||
type SidebarItemJSON =
|
||||
| string
|
||||
| SidebarCategoryShorthandJSON
|
||||
| SidebarItemDoc
|
||||
| SidebarItemLink
|
||||
| SidebarItemCategoryJSON
|
||||
| SidebarItemAutogeneratedJSON
|
||||
| {
|
||||
type: string;
|
||||
[key: string]: unknown;
|
||||
|
@ -56,7 +71,7 @@ function isCategoryShorthand(
|
|||
}
|
||||
|
||||
// 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
|
||||
|
@ -66,7 +81,7 @@ function normalizeCategoryShorthand(
|
|||
): SidebarItemCategoryJSON[] {
|
||||
return Object.entries(sidebar).map(([label, items]) => ({
|
||||
type: 'category',
|
||||
collapsed: defaultCategoryCollapsedValue,
|
||||
collapsed: DefaultCategoryCollapsedValue,
|
||||
label,
|
||||
items,
|
||||
}));
|
||||
|
@ -78,7 +93,7 @@ function normalizeCategoryShorthand(
|
|||
function assertItem<K extends string>(
|
||||
item: Record<string, unknown>,
|
||||
keys: K[],
|
||||
): asserts item is Record<K, never> {
|
||||
): asserts item is Record<K, unknown> {
|
||||
const unknownKeys = Object.keys(item).filter(
|
||||
// @ts-expect-error: key is always string
|
||||
(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(
|
||||
item: Record<string, unknown>,
|
||||
): asserts item is SidebarItemDoc {
|
||||
|
@ -152,7 +185,7 @@ function assertIsLink(
|
|||
* Normalizes recursively item and all its children. Ensures that at the end
|
||||
* each item will be an object with the corresponding type.
|
||||
*/
|
||||
function normalizeItem(item: SidebarItemJSON): SidebarItem[] {
|
||||
function normalizeItem(item: SidebarItemJSON): UnprocessedSidebarItem[] {
|
||||
if (typeof item === 'string') {
|
||||
return [
|
||||
{
|
||||
|
@ -169,11 +202,14 @@ function normalizeItem(item: SidebarItemJSON): SidebarItem[] {
|
|||
assertIsCategory(item);
|
||||
return [
|
||||
{
|
||||
collapsed: defaultCategoryCollapsedValue,
|
||||
collapsed: DefaultCategoryCollapsedValue,
|
||||
...item,
|
||||
items: flatMap(item.items, normalizeItem),
|
||||
},
|
||||
];
|
||||
case 'autogenerated':
|
||||
assertIsAutogenerated(item);
|
||||
return [item];
|
||||
case 'link':
|
||||
assertIsLink(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)
|
||||
? sidebar
|
||||
: normalizeCategoryShorthand(sidebar);
|
||||
|
@ -203,21 +239,29 @@ function normalizeSidebar(sidebar: SidebarJSON) {
|
|||
return flatMap(normalizedSidebar, normalizeItem);
|
||||
}
|
||||
|
||||
function normalizeSidebars(sidebars: SidebarsJSON): Sidebars {
|
||||
function normalizeSidebars(sidebars: SidebarsJSON): UnprocessedSidebars {
|
||||
return mapValues(sidebars, normalizeSidebar);
|
||||
}
|
||||
|
||||
export const DefaultSidebars: UnprocessedSidebars = {
|
||||
defaultSidebar: [
|
||||
{
|
||||
type: 'autogenerated',
|
||||
dirName: '.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// TODO refactor: make async
|
||||
export function loadSidebars(sidebarFilePath: string): Sidebars {
|
||||
export function loadSidebars(sidebarFilePath: string): UnprocessedSidebars {
|
||||
if (!sidebarFilePath) {
|
||||
throw new Error(`sidebarFilePath not provided: ${sidebarFilePath}`);
|
||||
}
|
||||
|
||||
// sidebars file is optional, some users use docs without sidebars!
|
||||
// See https://github.com/facebook/docusaurus/issues/3366
|
||||
// No sidebars file: by default we use the file-system structure to generate the sidebar
|
||||
// See https://github.com/facebook/docusaurus/pull/4582
|
||||
if (!fs.existsSync(sidebarFilePath)) {
|
||||
// throw new Error(`No sidebar file exist at path: ${sidebarFilePath}`);
|
||||
return {};
|
||||
return DefaultSidebars;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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<
|
||||
Type extends SidebarItemType,
|
||||
Item extends SidebarItem & {type: SidebarItemType}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue