fix(v2): improve dx sidebar config, ability to have no sidebars file (#4775)

* Improve sidebar config

* Edit message

* fix some little issues in the way undefined/false sidebars are handled

* remove old error message as it has been moved to a better place

Co-authored-by: Nam Hoang Le <nam.hoang.le@mgm-tp.com>
Co-authored-by: slorber <lorber.sebastien@gmail.com>
This commit is contained in:
Nam Hoang Le 2021-05-18 23:27:46 +07:00 committed by GitHub
parent e85ec1ab12
commit 1ab8aa0af8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 235 additions and 129 deletions

View file

@ -1,5 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`sidebar site with wrong sidebar content 1`] = `
"Bad sidebars file.
These sidebar document ids do not exist:
- goku,
Available document ids=
- foo/bar
- foo/baz
- headingAsTitle
- hello
- ipsum
- lorem
- rootAbsoluteSlug
- rootRelativeSlug
- rootResolvedSlug
- rootTryToEscapeSlug
- slugs/absoluteSlug
- slugs/relativeSlug
- slugs/resolvedSlug
- slugs/tryToEscapeSlug"
`;
exports[`simple website content 1`] = `
Object {
"docs": Array [
@ -790,28 +812,6 @@ Object {
}
`;
exports[`site with wrong sidebar file 1`] = `
"Bad sidebars file.
These sidebar document ids do not exist:
- goku,
Available document ids=
- foo/bar
- foo/baz
- headingAsTitle
- hello
- ipsum
- lorem
- rootAbsoluteSlug
- rootRelativeSlug
- rootResolvedSlug
- rootTryToEscapeSlug
- slugs/absoluteSlug
- slugs/relativeSlug
- slugs/resolvedSlug
- slugs/tryToEscapeSlug"
`;
exports[`versioned website (community) content: 100 version sidebars 1`] = `
Object {
"version-1.0.0/community": Array [

View file

@ -35,6 +35,7 @@ import {toSidebarsProp} from '../props';
import {validate} from 'webpack';
import {DefaultSidebarItemsGenerator} from '../sidebarItemsGenerator';
import {DisabledSidebars} from '../sidebars';
function findDocById(version: LoadedVersion, unversionedId: string) {
return version.docs.find((item) => item.unversionedId === unversionedId);
@ -124,17 +125,84 @@ Entries created:
};
};
test('site with wrong sidebar file', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'simple-site');
const context = await loadContext(siteDir);
const sidebarPath = path.join(siteDir, 'wrong-sidebars.json');
const plugin = pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
sidebarPath,
}),
);
await expect(plugin.loadContent!()).rejects.toThrowErrorMatchingSnapshot();
describe('sidebar', () => {
test('site with wrong sidebar content', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'simple-site');
const context = await loadContext(siteDir);
const sidebarPath = path.join(siteDir, 'wrong-sidebars.json');
const plugin = pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
sidebarPath,
}),
);
await expect(plugin.loadContent!()).rejects.toThrowErrorMatchingSnapshot();
});
test('site with wrong sidebar file path', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'site-with-doc-label');
const context = await loadContext(siteDir);
await expect(async () => {
const plugin = pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
sidebarPath: 'wrong-path-sidebar.json',
}),
);
await plugin.loadContent!();
}).rejects.toThrowErrorMatchingInlineSnapshot(`
"The path to the sidebar file does not exist at [wrong-path-sidebar.json].
Please set the docs [sidebarPath] field in your config file to:
- a sidebars path that exists
- false: to disable the sidebar
- undefined: for Docusaurus generates it automatically"
`);
});
test('site with undefined sidebar', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'site-with-doc-label');
const context = await loadContext(siteDir);
const plugin = pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
sidebarPath: undefined,
}),
);
const result = await plugin.loadContent!();
expect(result.loadedVersions).toHaveLength(1);
expect(result.loadedVersions[0].sidebars).toMatchInlineSnapshot(`
Object {
"defaultSidebar": Array [
Object {
"id": "hello-1",
"type": "doc",
},
Object {
"id": "hello-2",
"label": "Hello 2 From Doc",
"type": "doc",
},
],
}
`);
});
test('site with disabled sidebar', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'site-with-doc-label');
const context = await loadContext(siteDir);
const plugin = pluginContentDocs(
context,
normalizePluginOptions(OptionsSchema, {
sidebarPath: false,
}),
);
const result = await plugin.loadContent!();
expect(result.loadedVersions).toHaveLength(1);
expect(result.loadedVersions[0].sidebars).toEqual(DisabledSidebars);
});
});
describe('empty/no docs website', () => {

View file

@ -14,8 +14,9 @@ import {
collectSidebarCategories,
collectSidebarLinks,
transformSidebarItems,
DefaultSidebars,
processSidebars,
DefaultSidebars,
DisabledSidebars,
} from '../sidebars';
import {
Sidebar,
@ -127,35 +128,15 @@ describe('loadSidebars', () => {
});
test('unexisting path', () => {
/*
expect(() => loadSidebars('badpath')).toThrowErrorMatchingInlineSnapshot(
`"No sidebar file exist at path: badpath"`,
);
*/
// See https://github.com/facebook/docusaurus/issues/3366
expect(loadSidebars('badpath')).toEqual(DefaultSidebars);
expect(loadSidebars('badpath')).toEqual(DisabledSidebars);
});
test('undefined path', () => {
expect(() =>
loadSidebars(
// @ts-expect-error: bad arg
undefined,
),
).toThrowErrorMatchingInlineSnapshot(
`"sidebarFilePath not provided: undefined"`,
);
expect(loadSidebars(undefined)).toEqual(DefaultSidebars);
});
test('null path', () => {
expect(() =>
loadSidebars(
// @ts-expect-error: bad arg
null,
),
).toThrowErrorMatchingInlineSnapshot(
`"sidebarFilePath not provided: null"`,
);
test('literal false path', () => {
expect(loadSidebars(false)).toEqual(DisabledSidebars);
});
test('sidebars with category.collapsed property', async () => {

View file

@ -75,7 +75,7 @@ describe('simple site', () => {
),
isLast: true,
routePriority: -1,
sidebarFilePath: path.join(simpleSiteDir, 'sidebars.json'),
sidebarFilePath: undefined,
versionLabel: 'Next',
versionName: 'current',
versionPath: '/docs',
@ -138,6 +138,9 @@ describe('simple site', () => {
versionPath: '/myBaseUrl/docs/current-path',
versionLabel: 'current-label',
routePriority: undefined,
sidebarFilePath: undefined,
versionEditUrl: undefined,
versionEditUrlLocalized: undefined,
},
]);
});
@ -210,6 +213,7 @@ describe('versioned site, pluginId=default', () => {
const defaultOptions: PluginOptions = {
id: DEFAULT_PLUGIN_ID,
...DEFAULT_OPTIONS,
sidebarPath: 'sidebars.json',
};
const defaultContext = {
siteDir: versionedSiteDir,
@ -607,6 +611,7 @@ describe('versioned site, pluginId=community', () => {
id: 'community',
path: 'community',
routeBasePath: 'communityBasePath',
sidebarPath: 'sidebars.json',
};
const defaultContext = {
siteDir: versionedSiteDir,

View file

@ -20,6 +20,67 @@ import {
import {loadSidebars} from './sidebars';
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
function createVersionedSidebarFile({
siteDir,
pluginId,
sidebarPath,
version,
}: {
siteDir: string;
pluginId: string;
sidebarPath: string | false | undefined;
version: string;
}) {
// Load current sidebar and create a new versioned sidebars file (if needed).
const loadedSidebars = loadSidebars(sidebarPath);
// Do not create a useless versioned sidebars file if sidebars file is empty or sidebars are disabled/false)
const shouldCreateVersionedSidebarFile =
Object.keys(loadedSidebars).length > 0;
if (shouldCreateVersionedSidebarFile) {
// TODO @slorber: this "version prefix" in versioned sidebars looks like a bad idea to me
// TODO try to get rid of it
// Transform id in original sidebar to versioned id.
const normalizeItem = (
item: UnprocessedSidebarItem,
): UnprocessedSidebarItem => {
switch (item.type) {
case 'category':
return {...item, items: item.items.map(normalizeItem)};
case 'ref':
case 'doc':
return {
type: item.type,
id: `version-${version}/${item.id}`,
};
default:
return item;
}
};
const versionedSidebar: UnprocessedSidebars = Object.entries(
loadedSidebars,
).reduce((acc: UnprocessedSidebars, [sidebarId, sidebarItems]) => {
const newVersionedSidebarId = `version-${version}/${sidebarId}`;
acc[newVersionedSidebarId] = sidebarItems.map(normalizeItem);
return acc;
}, {});
const versionedSidebarsDir = getVersionedSidebarsDirPath(siteDir, pluginId);
const newSidebarFile = path.join(
versionedSidebarsDir,
`version-${version}-sidebars.json`,
);
fs.ensureDirSync(path.dirname(newSidebarFile));
fs.writeFileSync(
newSidebarFile,
`${JSON.stringify(versionedSidebar, null, 2)}\n`,
'utf8',
);
}
}
// Tests depend on non-default export for mocking.
// eslint-disable-next-line import/prefer-default-export
export function cliDocsVersionCommand(
@ -92,50 +153,7 @@ export function cliDocsVersionCommand(
throw new Error(`${pluginIdLogPrefix}There is no docs to version !`);
}
// Load current sidebar and create a new versioned sidebars file.
if (fs.existsSync(sidebarPath)) {
const loadedSidebars = loadSidebars(sidebarPath);
// TODO @slorber: this "version prefix" in versioned sidebars looks like a bad idea to me
// TODO try to get rid of it
// Transform id in original sidebar to versioned id.
const normalizeItem = (
item: UnprocessedSidebarItem,
): UnprocessedSidebarItem => {
switch (item.type) {
case 'category':
return {...item, items: item.items.map(normalizeItem)};
case 'ref':
case 'doc':
return {
type: item.type,
id: `version-${version}/${item.id}`,
};
default:
return item;
}
};
const versionedSidebar: UnprocessedSidebars = Object.entries(
loadedSidebars,
).reduce((acc: UnprocessedSidebars, [sidebarId, sidebarItems]) => {
const newVersionedSidebarId = `version-${version}/${sidebarId}`;
acc[newVersionedSidebarId] = sidebarItems.map(normalizeItem);
return acc;
}, {});
const versionedSidebarsDir = getVersionedSidebarsDirPath(siteDir, pluginId);
const newSidebarFile = path.join(
versionedSidebarsDir,
`version-${version}-sidebars.json`,
);
fs.ensureDirSync(path.dirname(newSidebarFile));
fs.writeFileSync(
newSidebarFile,
`${JSON.stringify(versionedSidebar, null, 2)}\n`,
'utf8',
);
}
createVersionedSidebarFile({siteDir, pluginId, version, sidebarPath});
// Update versions.json file.
versions.unshift(version);

View file

@ -118,8 +118,7 @@ export default function pluginContentDocs(
getPathsToWatch() {
function getVersionPathsToWatch(version: VersionMetadata): string[] {
return [
version.sidebarFilePath,
const result = [
...flatten(
options.include.map((pattern) =>
getDocsDirPaths(version).map(
@ -129,6 +128,10 @@ export default function pluginContentDocs(
),
`${version.contentPath}/**/${CategoryMetadataFilenamePattern}`,
];
if (typeof version.sidebarFilePath === 'string') {
result.unshift(version.sidebarFilePath);
}
return result;
}
return flatten(versionsMetadata.map(getVersionPathsToWatch));

View file

@ -21,12 +21,11 @@ import {
DisabledNumberPrefixParser,
} from './numberPrefix';
export const DEFAULT_OPTIONS: Omit<PluginOptions, 'id'> = {
export const DEFAULT_OPTIONS: Omit<PluginOptions, 'id' | 'sidebarPath'> = {
path: 'docs', // Path to data on filesystem, relative to site dir.
routeBasePath: 'docs', // URL Route.
homePageId: undefined, // TODO remove soon, deprecated
include: ['**/*.{md,mdx}'], // Extensions to include.
sidebarPath: 'sidebars.json', // Path to the sidebars configuration file
sidebarItemsGenerator: DefaultSidebarItemsGenerator,
numberPrefixParser: DefaultNumberPrefixParser,
docLayoutComponent: '@theme/DocPage',
@ -67,7 +66,10 @@ export const OptionsSchema = Joi.object({
.default(DEFAULT_OPTIONS.routeBasePath),
homePageId: Joi.string().optional(),
include: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.include),
sidebarPath: Joi.string().allow('').default(DEFAULT_OPTIONS.sidebarPath),
sidebarPath: Joi.alternatives().try(
Joi.boolean().invalid(true),
Joi.string(),
),
sidebarItemsGenerator: Joi.function().default(
() => DEFAULT_OPTIONS.sidebarItemsGenerator,
),

View file

@ -254,18 +254,29 @@ export const DefaultSidebars: UnprocessedSidebars = {
],
};
export const DisabledSidebars: UnprocessedSidebars = {};
// TODO refactor: make async
export function loadSidebars(sidebarFilePath: string): UnprocessedSidebars {
if (!sidebarFilePath) {
throw new Error(`sidebarFilePath not provided: ${sidebarFilePath}`);
export function loadSidebars(
sidebarFilePath: string | false | undefined,
): UnprocessedSidebars {
// false => no sidebars
if (sidebarFilePath === false) {
return DisabledSidebars;
}
// 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)) {
// undefined => defaults to autogenerated sidebars
if (typeof sidebarFilePath === 'undefined') {
return DefaultSidebars;
}
// unexisting sidebars file: no sidebars
// Note: this edge case can happen on versioned docs, not current version
// We avoid creating empty versioned sidebars file with the CLI
if (!fs.existsSync(sidebarFilePath)) {
return DisabledSidebars;
}
// We don't want sidebars to be cached because of hot reloading.
const sidebarJson = importFresh(sidebarFilePath) as SidebarsJSON;
return normalizeSidebars(sidebarJson);

View file

@ -31,9 +31,7 @@ export type VersionMetadata = ContentPaths & {
versionEditUrl?: string | undefined;
versionEditUrlLocalized?: string | undefined;
isLast: boolean;
// contentPath: string; // "versioned_docs/version-1.0.0"
// contentPathLocalized: string; // "i18n/fr/version-1.0.0/default"
sidebarFilePath: string; // versioned_sidebars/1.0.0.json
sidebarFilePath: string | false | undefined; // versioned_sidebars/1.0.0.json
routePriority: number | undefined; // -1 for the latest docs
};
@ -58,7 +56,7 @@ export type MetadataOptions = {
export type PathOptions = {
path: string;
sidebarPath: string;
sidebarPath?: string | false | undefined;
};
export type VersionOptions = {

View file

@ -24,7 +24,6 @@ import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
import {LoadContext} from '@docusaurus/types';
import {getPluginI18nPath, normalizeUrl, posixPath} from '@docusaurus/utils';
import {difference} from 'lodash';
import chalk from 'chalk';
// retro-compatibility: no prefix for the default plugin id
function addPluginIdPrefix(fileOrDir: string, pluginId: string): string {
@ -183,14 +182,24 @@ function getVersionMetadataPaths({
versionName,
});
const sidebarFilePath = isCurrentVersion
? path.resolve(context.siteDir, options.sidebarPath)
: path.join(
function getSidebarFilePath() {
if (isCurrentVersion) {
return options.sidebarPath
? path.resolve(context.siteDir, options.sidebarPath)
: options.sidebarPath;
} else {
return path.join(
getVersionedSidebarsDirPath(context.siteDir, options.id),
`version-${versionName}-sidebars.json`,
);
}
}
return {contentPath, contentPathLocalized, sidebarFilePath};
return {
contentPath,
contentPathLocalized,
sidebarFilePath: getSidebarFilePath(),
};
}
function getVersionEditUrls({
@ -330,6 +339,7 @@ function checkVersionMetadataPaths({
}) {
const {versionName, contentPath, sidebarFilePath} = versionMetadata;
const {siteDir} = context;
const isCurrentVersion = versionName === CURRENT_VERSION_NAME;
if (!fs.existsSync(contentPath)) {
throw new Error(
@ -340,13 +350,23 @@ function checkVersionMetadataPaths({
);
}
// If the current version defines a path to a sidebar file that does not exist, we throw!
// Note: for versioned sidebars, the file may not exist (as we prefer to not create it rather than to create an empty file)
// See https://github.com/facebook/docusaurus/issues/3366
if (!fs.existsSync(sidebarFilePath)) {
console.log(
chalk.yellow(
`The sidebar file of docs version [${versionName}] does not exist. It is optional, but should rather be provided at ${sidebarFilePath}`,
),
);
// See https://github.com/facebook/docusaurus/pull/4775
if (
isCurrentVersion &&
typeof sidebarFilePath === 'string' &&
!fs.existsSync(sidebarFilePath)
) {
throw new Error(`The path to the sidebar file does not exist at [${path.relative(
siteDir,
sidebarFilePath,
)}].
Please set the docs [sidebarPath] field in your config file to:
- a sidebars path that exists
- false: to disable the sidebar
- undefined: for Docusaurus generates it automatically`);
}
}