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

* POC of autogenerated sidebars

* use combine-promises utility lib

* autogenerated sidebar poc working

* Revert "autogenerated sidebar poc working"

This reverts commit c81da980

* POC of auto-generated sidebars for community docs

* update tests

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

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

* fix typo!

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

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

* fix tests

* change position of API

* ability to extract number prefix

* stable system to enable position frontmatter

* fix tests for autogen sidebar position

* renamings

* restore community sidebars

* rename frontmatter position -> sidebar_position

* make sidebarItemsGenerator fn configurable

* minor changes

* rename dirPath => dirName

* Make the init template use autogenerated sidebars

* fix options

* fix docusaurus site: remove test docs

* add _category_ file to docs pathsToWatch

* add _category_ file to docs pathsToWatch

* tutorial: use sidebar_position instead of file number prefixes

* Adapt Docusaurus tutorial for autogenerated sidebars

* remove slug: /

* polish the homepage template

* rename _category_ sidebar_position to just "position"

* test for custom sidebarItemsGenerator fn

* fix category metadata + add link to report tutorial issues

* fix absolute path breaking tests

* fix absolute path breaking tests

* Add test for floating number sidebar_position

* add sidebarItemsGenerator unit tests

* add processSidebars unit tests

* Fix init template broken links

* windows test

* increase code translations test timeout

* cleanup mockCategoryMetadataFiles after windows test fixed

* update init template positions

* fix windows tests

* fix comment

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

* add useful comment

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

View file

@ -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}