feat(v2): allow non sidebar category to be first item of sidebar (#2032)

* feat(v2): allow non sidebar category to be first item of sidebar

* better error messages

* edit the react component

* Update website/docs/sidebar.md

* nits

* add @babel/plugin-transform-runtime
This commit is contained in:
Endi 2019-11-24 11:08:19 +07:00 committed by Yangshun Tay
parent c533adc4aa
commit 9862a6821a
20 changed files with 849 additions and 671 deletions

View file

@ -0,0 +1,11 @@
{
"docs": {
"Test": [
{
"type": "category",
"label": true,
"items": ["doc1"]
}
]
}
}

View file

@ -0,0 +1,10 @@
{
"docs": {
"Test": [
{
"type": "doc",
"id": ["doc1"]
}
]
}
}

View file

@ -0,0 +1,8 @@
{
"docs": [
{
"a": "b",
"c": "d"
}
]
}

View file

@ -0,0 +1,11 @@
{
"docs": {
"Test": [
{
"type": "link",
"label": "GitHub",
"href": ["example.com"]
}
]
}
}

View file

@ -0,0 +1,11 @@
{
"docs": {
"Test": [
{
"type": "link",
"label": false,
"href": "https://github.com"
}
]
}
}

View file

@ -0,0 +1,11 @@
{
"docs": {
"Test": [
{
"type": "link",
"label": "category",
"href": "https://github.com"
}
]
}
}

View file

@ -1,5 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`loadSidebars sidebars link 1`] = `
Object {
"docs": Array [
Object {
"items": Array [
Object {
"href": "https://github.com",
"label": "category",
"type": "link",
},
],
"label": "Test",
"type": "category",
},
],
}
`;
exports[`loadSidebars sidebars with deep level of category 1`] = `
Object {
"docs": Array [
@ -57,6 +75,27 @@ Object {
}
`;
exports[`loadSidebars sidebars with first level not a category 1`] = `
Object {
"docs": Array [
Object {
"items": Array [
Object {
"id": "greeting",
"type": "doc",
},
],
"label": "Getting Started",
"type": "category",
},
Object {
"id": "api",
"type": "doc",
},
],
}
`;
exports[`loadSidebars sidebars with known sidebar item type 1`] = `
Object {
"docs": Array [

View file

@ -32,7 +32,31 @@ describe('loadSidebars', () => {
expect(() =>
loadSidebars([sidebarPath]),
).toThrowErrorMatchingInlineSnapshot(
`"Error loading \\"Category Label\\" category. Category items must be array."`,
`"Error loading {\\"type\\":\\"category\\",\\"label\\":\\"Category Label\\",\\"items\\":\\"doc1\\"}. \\"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',
);
expect(() =>
loadSidebars([sidebarPath]),
).toThrowErrorMatchingInlineSnapshot(
`"Error loading {\\"type\\":\\"category\\",\\"label\\":true,\\"items\\":[\\"doc1\\"]}. \\"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',
);
expect(() =>
loadSidebars([sidebarPath]),
).toThrowErrorMatchingInlineSnapshot(
`"Error loading {\\"type\\":\\"doc\\",\\"id\\":[\\"doc1\\"]}. \\"id\\" must be a string."`,
);
});
@ -41,10 +65,40 @@ describe('loadSidebars', () => {
fixtureDir,
'sidebars-first-level-not-category.js',
);
const result = loadSidebars([sidebarPath]);
expect(result).toMatchSnapshot();
});
test('sidebars link', async () => {
const sidebarPath = path.join(fixtureDir, 'sidebars-link.json');
const result = loadSidebars([sidebarPath]);
expect(result).toMatchSnapshot();
});
test('sidebars link wrong label', async () => {
const sidebarPath = path.join(fixtureDir, 'sidebars-link-wrong-label.json');
expect(() =>
loadSidebars([sidebarPath]),
).toThrowErrorMatchingInlineSnapshot(
`"Error loading {\\"type\\":\\"doc\\",\\"id\\":\\"api\\"}. First level item of a sidebar must be a category"`,
`"Error loading {\\"type\\":\\"link\\",\\"label\\":false,\\"href\\":\\"https://github.com\\"}. \\"label\\" must be a string."`,
);
});
test('sidebars link wrong href', async () => {
const sidebarPath = path.join(fixtureDir, 'sidebars-link-wrong-href.json');
expect(() =>
loadSidebars([sidebarPath]),
).toThrowErrorMatchingInlineSnapshot(
`"Error loading {\\"type\\":\\"link\\",\\"label\\":\\"GitHub\\",\\"href\\":[\\"example.com\\"]}. \\"href\\" must be a string."`,
);
});
test('sidebars with invalid sidebar item', async () => {
const sidebarPath = path.join(fixtureDir, 'sidebars-invalid-item.json');
expect(() =>
loadSidebars([sidebarPath]),
).toThrowErrorMatchingInlineSnapshot(
`"Unknown sidebar item \\"{\\"a\\":\\"b\\",\\"c\\":\\"d\\"}\\"."`,
);
});

View file

@ -25,16 +25,16 @@ import {
LoadedContent,
SourceToPermalink,
PermalinkToSidebar,
DocsSidebarItemCategory,
SidebarItemLink,
SidebarItemDoc,
SidebarItemCategory,
DocsSidebar,
DocsBaseMetadata,
MetadataRaw,
DocsMetadataRaw,
Metadata,
VersionToSidebars,
SidebarItem,
DocsSidebarItem,
} from './types';
import {Configuration} from 'webpack';
import {docsVersion} from './version';
@ -237,32 +237,24 @@ export default function pluginContentDocs(
};
};
const normalizeCategory = (
category: SidebarItemCategory,
): DocsSidebarItemCategory => {
const items = category.items.map(item => {
switch (item.type) {
case 'category':
return normalizeCategory(item as SidebarItemCategory);
case 'ref':
case 'doc':
return convertDocLink(item as SidebarItemDoc);
case 'link':
default:
break;
}
return item as SidebarItemLink;
});
return {...category, items};
const normalizeItem = (item: SidebarItem): DocsSidebarItem => {
switch (item.type) {
case 'category':
return {...item, items: item.items.map(normalizeItem)};
case 'ref':
case 'doc':
return convertDocLink(item);
case 'link':
default:
return item;
}
};
// Transform the sidebar so that all sidebar item will be in the form of 'link' or 'category' only
// This is what will be passed as props to the UI component
const docsSidebars: DocsSidebar = Object.entries(loadedSidebars).reduce(
(acc: DocsSidebar, [sidebarId, sidebarItemCategories]) => {
acc[sidebarId] = sidebarItemCategories.map(sidebarItemCategory =>
normalizeCategory(sidebarItemCategory),
);
(acc: DocsSidebar, [sidebarId, sidebarItems]) => {
acc[sidebarId] = sidebarItems.map(normalizeItem);
return acc;
},
{},

View file

@ -8,11 +8,13 @@
import fs from 'fs-extra';
import importFresh from 'import-fresh';
import {
SidebarItemCategory,
Sidebar,
SidebarRaw,
SidebarItem,
SidebarItemCategoryRaw,
SidebarItemRaw,
SidebarItemLink,
SidebarItemDoc,
} from './types';
/**
@ -32,54 +34,71 @@ function assertItem(item: Object, keys: string[]): void {
}
}
function assertIsCategory(item: any): asserts item is SidebarItemCategoryRaw {
assertItem(item, ['items', 'label']);
if (typeof item.label !== 'string') {
throw new Error(
`Error loading ${JSON.stringify(item)}. "label" must be a string.`,
);
}
if (!Array.isArray(item.items)) {
throw new Error(
`Error loading ${JSON.stringify(item)}. "items" must be an array.`,
);
}
}
function assertIsDoc(item: any): asserts item is SidebarItemDoc {
assertItem(item, ['id']);
if (typeof item.id !== 'string') {
throw new Error(
`Error loading ${JSON.stringify(item)}. "id" must be a string.`,
);
}
}
function assertIsLink(item: any): asserts item is SidebarItemLink {
assertItem(item, ['href', 'label']);
if (typeof item.href !== 'string') {
throw new Error(
`Error loading ${JSON.stringify(item)}. "href" must be a string.`,
);
}
if (typeof item.label !== 'string') {
throw new Error(
`Error loading ${JSON.stringify(item)}. "label" must be a string.`,
);
}
}
/**
* Normalizes recursively category and all its children. Ensures, that at the end
* Normalizes recursively item and all its children. Ensures, that at the end
* each item will be an object with the corresponding type
*/
function normalizeCategory(
category: SidebarItemCategoryRaw,
level = 0,
): SidebarItemCategory {
if (level === 0 && category.type !== 'category') {
throw new Error(
`Error loading ${JSON.stringify(
category,
)}. First level item of a sidebar must be a category`,
);
function normalizeItem(item: SidebarItemRaw): SidebarItem {
if (typeof item === 'string') {
return {
type: 'doc',
id: item,
};
}
assertItem(category, ['items', 'label']);
if (!Array.isArray(category.items)) {
throw new Error(
`Error loading "${category.label}" category. Category items must be array.`,
);
if (!item.type) {
throw new Error(`Unknown sidebar item "${JSON.stringify(item)}".`);
}
switch (item.type) {
case 'category':
assertIsCategory(item);
return {...item, items: item.items.map(normalizeItem)};
case 'link':
assertIsLink(item);
return item;
case 'ref':
case 'doc':
assertIsDoc(item);
return item;
default:
throw new Error(`Unknown sidebar item type: ${item.type}`);
}
const items: SidebarItem[] = category.items.map(item => {
if (typeof item === 'string') {
return {
type: 'doc',
id: item,
};
}
switch (item.type) {
case 'category':
return normalizeCategory(item as SidebarItemCategoryRaw, level + 1);
case 'link':
assertItem(item, ['href', 'label']);
break;
case 'ref':
case 'doc':
assertItem(item, ['id']);
break;
default:
throw new Error(`Unknown sidebar item type: ${item.type}`);
}
return item as SidebarItem;
});
return {...category, items};
}
/**
@ -88,7 +107,7 @@ function normalizeCategory(
function normalizeSidebar(sidebars: SidebarRaw): Sidebar {
return Object.entries(sidebars).reduce(
(acc: Sidebar, [sidebarId, sidebar]) => {
let normalizedSidebar: SidebarItemCategoryRaw[];
let normalizedSidebar: SidebarItemRaw[];
if (!Array.isArray(sidebar)) {
// convert sidebar to a more generic structure
@ -101,7 +120,7 @@ function normalizeSidebar(sidebars: SidebarRaw): Sidebar {
normalizedSidebar = sidebar;
}
acc[sidebarId] = normalizedSidebar.map(item => normalizeCategory(item));
acc[sidebarId] = normalizedSidebar.map(normalizeItem);
return acc;
},

View file

@ -65,23 +65,27 @@ export type SidebarItemRaw =
// Sidebar given by user that is not normalized yet. e.g: sidebars.json
export interface SidebarRaw {
[sidebarId: string]: {
[sidebarCategory: string]: SidebarItemRaw[];
};
[sidebarId: string]:
| {
[sidebarCategory: string]: SidebarItemRaw[];
}
| SidebarItemRaw[];
}
export interface Sidebar {
[sidebarId: string]: SidebarItemCategory[];
[sidebarId: string]: SidebarItem[];
}
export interface DocsSidebarItemCategory {
type: 'category';
label: string;
items: (SidebarItemLink | DocsSidebarItemCategory)[];
items: DocsSidebarItem[];
}
export type DocsSidebarItem = SidebarItemLink | DocsSidebarItemCategory;
export interface DocsSidebar {
[sidebarId: string]: DocsSidebarItemCategory[];
[sidebarId: string]: DocsSidebarItem[];
}
export interface OrderMetadata {

View file

@ -12,7 +12,7 @@ import {
} from './env';
import fs from 'fs-extra';
import path from 'path';
import {Sidebar, SidebarItemCategory, PathOptions} from './types';
import {Sidebar, PathOptions, SidebarItem} from './types';
import loadSidebars from './sidebars';
export function docsVersion(
@ -80,33 +80,25 @@ export function docsVersion(
const loadedSidebars: Sidebar = loadSidebars([sidebarPath]);
// Transform id in original sidebar to versioned id
const normalizeCategory = (
category: SidebarItemCategory,
): SidebarItemCategory => {
const items = category.items.map(item => {
switch (item.type) {
case 'category':
return normalizeCategory(item);
case 'ref':
case 'doc':
return {
type: item.type,
id: `version-${version}/${item.id}`,
};
}
return item;
});
return {...category, items};
const normalizeItem = (item: SidebarItem): SidebarItem => {
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: Sidebar = Object.entries(loadedSidebars).reduce(
(acc: Sidebar, [sidebarId, sidebarItemCategories]) => {
(acc: Sidebar, [sidebarId, sidebarItems]) => {
const newVersionedSidebarId = `version-${version}/${sidebarId}`;
acc[
newVersionedSidebarId
] = sidebarItemCategories.map(sidebarItemCategory =>
normalizeCategory(sidebarItemCategory),
);
acc[newVersionedSidebarId] = sidebarItems.map(normalizeItem);
return acc;
},
{},