feat(v2): global data + useGlobalData + docs versions dropdown (#2971)

* doc components initial simplification

* doc components initial simplification

* add docContext test

* Add poc of global data system + use it in the theme

* Revert "doc components initial simplification"

This reverts commit f657b4c4

* revert useless changes

* avoid loosing context on docs switch

* fix docs tests

* fix @generated/globalData ts declaration / es import

* typo

* revert bad commit

* refactor navbar in multiple parts + add navbar item types validation + try to fix remaining merge bugs

* add missing watch mode for plugin debug

* fix docs global data integration, move related hooks to docs plugin + convert to TS

* change versions link label

* fix activeClassName react warning

* improve docs global data system + contextual navbar dropdown

* fix bug preventing the deployment

* refactor the global data system to namespace automatically by plugin name + plugin id

* proper NavbarItem comp

* fix tests

* fix snapshot

* extract theme config schema in separate file + rename navbar links to navbar items

* minor typos

* polish docs components/api

* polish useDocs api surface

* fix the docs version suggestions comp + data

* refactors + add docsClientUtils unit tests

* Add documentation

* typo

* Add check for duplicate plugin ids detection

* multi-instance: createData plugin data should be namespaced by plugin instance id

* remove attempt for multi-instance support
This commit is contained in:
Sébastien Lorber 2020-07-21 11:16:08 +02:00 committed by GitHub
parent a51a56ec42
commit 15e73daae7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 1954 additions and 531 deletions

View file

@ -109,6 +109,46 @@ Array [
]
`;
exports[`simple website content 3`] = `
Object {
"pluginName": Object {
"pluginId": Object {
"latestVersionName": null,
"path": "docs",
"versions": Array [
Object {
"docs": Array [
Object {
"id": "foo/bar",
"path": "/docs/foo/bar",
},
Object {
"id": "foo/baz",
"path": "/docs/foo/bazSlug.html",
},
Object {
"id": "hello",
"path": "/docs/",
},
Object {
"id": "ipsum",
"path": "/docs/ipsum",
},
Object {
"id": "lorem",
"path": "/docs/lorem",
},
],
"mainDocId": "hello",
"name": null,
"path": "/docs",
},
],
},
},
}
`;
exports[`site with wrong sidebar file 1`] = `
"Bad sidebars file. The document id 'goku' was used in the sidebar, but no document with this id could be found.
Available document ids=
@ -213,6 +253,68 @@ Array [
]
`;
exports[`versioned website content 2`] = `
Object {
"pluginName": Object {
"pluginId": Object {
"latestVersionName": "1.0.1",
"path": "docs",
"versions": Array [
Object {
"docs": Array [
Object {
"id": "foo/bar",
"path": "/docs/next/foo/barSlug",
},
Object {
"id": "hello",
"path": "/docs/next/",
},
],
"mainDocId": "hello",
"name": "next",
"path": "/docs/next",
},
Object {
"docs": Array [
Object {
"id": "foo/bar",
"path": "/docs/foo/bar",
},
Object {
"id": "hello",
"path": "/docs/",
},
],
"mainDocId": "hello",
"name": "1.0.1",
"path": "/docs",
},
Object {
"docs": Array [
Object {
"id": "foo/bar",
"path": "/docs/1.0.0/foo/barSlug",
},
Object {
"id": "foo/baz",
"path": "/docs/1.0.0/foo/baz",
},
Object {
"id": "hello",
"path": "/docs/1.0.0/",
},
],
"mainDocId": "hello",
"name": "1.0.0",
"path": "/docs/1.0.0",
},
],
},
},
}
`;
exports[`versioned website content: all sidebars 1`] = `
Object {
"docs": Array [

View file

@ -0,0 +1,361 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
ActivePlugin,
getActivePlugin,
getLatestVersion,
getActiveDocContext,
getActiveVersion,
getDocVersionSuggestions,
} from '../../client/docsClientUtils';
import {GlobalPluginData, GlobalVersion} from '../../types';
import {shuffle} from 'lodash';
describe('docsClientUtils', () => {
test('getActivePlugin', () => {
const data: Record<string, GlobalPluginData> = {
pluginIosId: {
path: 'ios',
latestVersionName: 'xyz',
versions: [],
},
pluginAndroidId: {
path: 'android',
latestVersionName: 'xyz',
versions: [],
},
};
expect(getActivePlugin(data, '/')).toEqual(undefined);
expect(getActivePlugin(data, '/xyz')).toEqual(undefined);
const activePluginIos: ActivePlugin = {
pluginId: 'pluginIosId',
pluginData: data.pluginIosId,
};
expect(getActivePlugin(data, '/ios')).toEqual(activePluginIos);
expect(getActivePlugin(data, '/ios/')).toEqual(activePluginIos);
expect(getActivePlugin(data, '/ios/abc/def')).toEqual(activePluginIos);
const activePluginAndroid: ActivePlugin = {
pluginId: 'pluginAndroidId',
pluginData: data.pluginAndroidId,
};
expect(getActivePlugin(data, '/android')).toEqual(activePluginAndroid);
expect(getActivePlugin(data, '/android/')).toEqual(activePluginAndroid);
expect(getActivePlugin(data, '/android/ijk')).toEqual(activePluginAndroid);
});
test('getLatestVersion', () => {
const versions: GlobalVersion[] = [
{
name: 'version1',
path: '/???',
docs: [],
mainDocId: '???',
},
{
name: 'version2',
path: '/???',
docs: [],
mainDocId: '???',
},
{
name: 'version3',
path: '/???',
docs: [],
mainDocId: '???',
},
];
expect(
getLatestVersion({
path: '???',
latestVersionName: 'does not exist',
versions,
}),
).toEqual(undefined);
expect(
getLatestVersion({
path: '???',
latestVersionName: 'version1',
versions,
})?.name,
).toEqual('version1');
expect(
getLatestVersion({
path: '???',
latestVersionName: 'version2',
versions,
})?.name,
).toEqual('version2');
expect(
getLatestVersion({
path: '???',
latestVersionName: 'version3',
versions,
})?.name,
).toEqual('version3');
});
test('getActiveVersion', () => {
const data: GlobalPluginData = {
path: 'docs',
latestVersionName: 'version2',
versions: [
{
name: 'next',
path: '/docs/next',
docs: [],
mainDocId: '???',
},
{
name: 'version2',
path: '/docs',
docs: [],
mainDocId: '???',
},
{
name: 'version1',
path: '/docs/version1',
docs: [],
mainDocId: '???',
},
],
};
expect(getActiveVersion(data, '/docs/next')?.name).toEqual('next');
expect(getActiveVersion(data, '/docs/next/')?.name).toEqual('next');
expect(getActiveVersion(data, '/docs/next/someDoc')?.name).toEqual('next');
expect(getActiveVersion(data, '/docs')?.name).toEqual('version2');
expect(getActiveVersion(data, '/docs/')?.name).toEqual('version2');
expect(getActiveVersion(data, '/docs/someDoc')?.name).toEqual('version2');
expect(getActiveVersion(data, '/docs/version1')?.name).toEqual('version1');
expect(getActiveVersion(data, '/docs/version1')?.name).toEqual('version1');
expect(getActiveVersion(data, '/docs/version1/someDoc')?.name).toEqual(
'version1',
);
});
test('getActiveDocContext', () => {
const versionNext: GlobalVersion = {
name: 'next',
path: '/docs/next',
mainDocId: 'doc1',
docs: [
{
id: 'doc1',
path: '/docs/next/',
},
{
id: 'doc2',
path: '/docs/next/doc2',
},
],
};
const version2: GlobalVersion = {
name: 'version2',
path: '/docs',
mainDocId: 'doc1',
docs: [
{
id: 'doc1',
path: '/docs/',
},
{
id: 'doc2',
path: '/docs/doc2',
},
],
};
const version1: GlobalVersion = {
name: 'version1',
path: '/docs/version1',
mainDocId: 'doc1',
docs: [
{
id: 'doc1',
path: '/docs/version1/',
},
],
};
// shuffle, because order shouldn't matter
const versions: GlobalVersion[] = shuffle([
versionNext,
version2,
version1,
]);
const data: GlobalPluginData = {
path: 'docs',
latestVersionName: 'version2',
versions,
};
expect(getActiveDocContext(data, '/doesNotExist')).toEqual({
activeVersion: undefined,
activeDoc: undefined,
alternateDocVersions: {},
});
expect(getActiveDocContext(data, '/docs/next/doesNotExist')).toEqual({
activeVersion: versionNext,
activeDoc: undefined,
alternateDocVersions: {},
});
expect(getActiveDocContext(data, '/docs/next')).toEqual({
activeVersion: versionNext,
activeDoc: versionNext.docs[0],
alternateDocVersions: {
next: versionNext.docs[0],
version2: version2.docs[0],
version1: version1.docs[0],
},
});
expect(getActiveDocContext(data, '/docs/next/doc2')).toEqual({
activeVersion: versionNext,
activeDoc: versionNext.docs[1],
alternateDocVersions: {
next: versionNext.docs[1],
version2: version2.docs[1],
version1: undefined,
},
});
expect(getActiveDocContext(data, '/docs/')).toEqual({
activeVersion: version2,
activeDoc: version2.docs[0],
alternateDocVersions: {
next: versionNext.docs[0],
version2: version2.docs[0],
version1: version1.docs[0],
},
});
expect(getActiveDocContext(data, '/docs/doc2')).toEqual({
activeVersion: version2,
activeDoc: version2.docs[1],
alternateDocVersions: {
next: versionNext.docs[1],
version2: version2.docs[1],
version1: undefined,
},
});
expect(getActiveDocContext(data, '/docs/version1')).toEqual({
activeVersion: version1,
activeDoc: version1.docs[0],
alternateDocVersions: {
next: versionNext.docs[0],
version2: version2.docs[0],
version1: version1.docs[0],
},
});
expect(getActiveDocContext(data, '/docs/version1/doc2')).toEqual({
activeVersion: version1,
activeDoc: undefined,
alternateDocVersions: {},
});
});
test('getDocVersionSuggestions', () => {
const versionNext: GlobalVersion = {
name: 'next',
path: '/docs/next',
mainDocId: 'doc1',
docs: [
{
id: 'doc1',
path: '/docs/next/',
},
{
id: 'doc2',
path: '/docs/next/doc2',
},
],
};
const version2: GlobalVersion = {
name: 'version2',
path: '/docs',
mainDocId: 'doc1',
docs: [
{
id: 'doc1',
path: '/docs/',
},
{
id: 'doc2',
path: '/docs/doc2',
},
],
};
const version1: GlobalVersion = {
name: 'version1',
path: '/docs/version1',
mainDocId: 'doc1',
docs: [
{
id: 'doc1',
path: '/docs/version1/',
},
],
};
// shuffle, because order shouldn't matter
const versions: GlobalVersion[] = shuffle([
versionNext,
version2,
version1,
]);
const data: GlobalPluginData = {
path: 'docs',
latestVersionName: 'version2',
versions,
};
expect(getDocVersionSuggestions(data, '/doesNotExist')).toEqual({
latestDocSuggestion: undefined,
latestVersionSuggestion: version2,
});
expect(getDocVersionSuggestions(data, '/docs/next')).toEqual({
latestDocSuggestion: version2.docs[0],
latestVersionSuggestion: version2,
});
expect(getDocVersionSuggestions(data, '/docs/next/doc2')).toEqual({
latestDocSuggestion: version2.docs[1],
latestVersionSuggestion: version2,
});
// nothing to suggest, we are already on latest version
expect(getDocVersionSuggestions(data, '/docs/')).toEqual({
latestDocSuggestion: undefined,
latestVersionSuggestion: undefined,
});
expect(getDocVersionSuggestions(data, '/docs/doc2')).toEqual({
latestDocSuggestion: undefined,
latestVersionSuggestion: undefined,
});
expect(getDocVersionSuggestions(data, '/docs/version1/')).toEqual({
latestDocSuggestion: version2.docs[0],
latestVersionSuggestion: version2,
});
expect(getDocVersionSuggestions(data, '/docs/version1/doc2')).toEqual({
latestDocSuggestion: undefined, // because /docs/version1/doc2 does not exist
latestVersionSuggestion: version2,
});
});
});

View file

@ -25,6 +25,7 @@ const createFakeActions = (
routeConfigs: RouteConfig[],
contentDir,
dataContainer?,
globalDataContainer?,
) => {
return {
addRoute: (config: RouteConfig) => {
@ -36,6 +37,9 @@ const createFakeActions = (
}
return path.join(contentDir, name);
},
setGlobalData: (data) => {
globalDataContainer.pluginName = {pluginId: data};
},
};
};
@ -166,6 +170,7 @@ describe('simple website', () => {
expect(versionToSidebars).toEqual({});
expect(docsMetadata.hello).toEqual({
id: 'hello',
unversionedId: 'hello',
isDocsHomePage: true,
permalink: '/docs/',
previous: {
@ -176,11 +181,11 @@ describe('simple website', () => {
source: path.join('@site', pluginPath, 'hello.md'),
title: 'Hello, World !',
description: 'Hi, Endilie here :)',
latestVersionMainDocPermalink: undefined,
});
expect(docsMetadata['foo/bar']).toEqual({
id: 'foo/bar',
unversionedId: 'foo/bar',
isDocsHomePage: false,
next: {
title: 'baz',
@ -191,17 +196,18 @@ describe('simple website', () => {
source: path.join('@site', pluginPath, 'foo', 'bar.md'),
title: 'Bar',
description: 'This is custom description',
latestVersionMainDocPermalink: undefined,
});
expect(docsSidebars).toMatchSnapshot();
const routeConfigs = [];
const dataContainer = {};
const globalDataContainer = {};
const actions = createFakeActions(
routeConfigs,
pluginContentDir,
dataContainer,
globalDataContainer,
);
await plugin.contentLoaded({
@ -219,6 +225,7 @@ describe('simple website', () => {
expect(routeConfigs).not.toEqual([]);
expect(routeConfigs).toMatchSnapshot();
expect(globalDataContainer).toMatchSnapshot();
});
});
@ -313,6 +320,7 @@ describe('versioned website', () => {
expect(docsMetadata['version-1.0.1/foo/baz']).toBeUndefined();
expect(docsMetadata['foo/bar']).toEqual({
id: 'foo/bar',
unversionedId: 'foo/bar',
isDocsHomePage: false,
permalink: '/docs/next/foo/barSlug',
source: path.join('@site', routeBasePath, 'foo', 'bar.md'),
@ -327,6 +335,7 @@ describe('versioned website', () => {
});
expect(docsMetadata.hello).toEqual({
id: 'hello',
unversionedId: 'hello',
isDocsHomePage: true,
permalink: '/docs/next/',
source: path.join('@site', routeBasePath, 'hello.md'),
@ -341,6 +350,7 @@ describe('versioned website', () => {
});
expect(docsMetadata['version-1.0.1/hello']).toEqual({
id: 'version-1.0.1/hello',
unversionedId: 'hello',
isDocsHomePage: true,
permalink: '/docs/',
source: path.join(
@ -357,10 +367,10 @@ describe('versioned website', () => {
title: 'bar',
permalink: '/docs/foo/bar',
},
latestVersionMainDocPermalink: undefined,
});
expect(docsMetadata['version-1.0.0/foo/baz']).toEqual({
id: 'version-1.0.0/foo/baz',
unversionedId: 'foo/baz',
isDocsHomePage: false,
permalink: '/docs/1.0.0/foo/baz',
source: path.join(
@ -391,10 +401,12 @@ describe('versioned website', () => {
);
const routeConfigs = [];
const dataContainer = {};
const globalDataContainer = {};
const actions = createFakeActions(
routeConfigs,
pluginContentDir,
dataContainer,
globalDataContainer,
);
await plugin.contentLoaded({
content,
@ -438,5 +450,6 @@ describe('versioned website', () => {
expect(routeConfigs).not.toEqual([]);
expect(routeConfigs).toMatchSnapshot();
expect(globalDataContainer).toMatchSnapshot();
});
});

View file

@ -46,21 +46,21 @@ describe('simple site', () => {
expect(dataA).toEqual({
id: 'foo/bar',
unversionedId: 'foo/bar',
isDocsHomePage: false,
permalink: '/docs/foo/bar',
source: path.join('@site', routeBasePath, sourceA),
title: 'Bar',
description: 'This is custom description',
latestVersionMainDocPermalink: undefined,
});
expect(dataB).toEqual({
id: 'hello',
unversionedId: 'hello',
isDocsHomePage: false,
permalink: '/docs/hello',
source: path.join('@site', routeBasePath, sourceB),
title: 'Hello, World !',
description: `Hi, Endilie here :)`,
latestVersionMainDocPermalink: undefined,
});
});
@ -81,6 +81,7 @@ describe('simple site', () => {
expect(data).toEqual({
id: 'hello',
unversionedId: 'hello',
isDocsHomePage: true,
permalink: '/docs/',
source: path.join('@site', routeBasePath, source),
@ -106,6 +107,7 @@ describe('simple site', () => {
expect(data).toEqual({
id: 'foo/bar',
unversionedId: 'foo/bar',
isDocsHomePage: true,
permalink: '/docs/',
source: path.join('@site', routeBasePath, source),
@ -133,6 +135,7 @@ describe('simple site', () => {
expect(data).toEqual({
id: 'foo/baz',
unversionedId: 'foo/baz',
isDocsHomePage: false,
permalink: '/docs/foo/bazSlug.html',
source: path.join('@site', routeBasePath, source),
@ -140,7 +143,6 @@ describe('simple site', () => {
editUrl:
'https://github.com/facebook/docusaurus/edit/master/website/docs/foo/baz.md',
description: 'Images',
latestVersionMainDocPermalink: undefined,
});
});
@ -160,13 +162,13 @@ describe('simple site', () => {
expect(data).toEqual({
id: 'lorem',
unversionedId: 'lorem',
isDocsHomePage: false,
permalink: '/docs/lorem',
source: path.join('@site', routeBasePath, source),
title: 'lorem',
editUrl: 'https://github.com/customUrl/docs/lorem.md',
description: 'Lorem ipsum.',
latestVersionMainDocPermalink: undefined,
});
// unrelated frontmatter is not part of metadata
@ -192,6 +194,7 @@ describe('simple site', () => {
expect(data).toEqual({
id: 'lorem',
unversionedId: 'lorem',
isDocsHomePage: false,
permalink: '/docs/lorem',
source: path.join('@site', routeBasePath, source),
@ -200,7 +203,6 @@ describe('simple site', () => {
description: 'Lorem ipsum.',
lastUpdatedAt: 1539502055,
lastUpdatedBy: 'Author',
latestVersionMainDocPermalink: undefined,
});
});
@ -222,6 +224,7 @@ describe('simple site', () => {
expect(data).toEqual({
id: 'ipsum',
unversionedId: 'ipsum',
isDocsHomePage: false,
permalink: '/docs/ipsum',
source: path.join('@site', routeBasePath, source),
@ -230,7 +233,6 @@ describe('simple site', () => {
description: 'Lorem ipsum.',
lastUpdatedAt: 1539502055,
lastUpdatedBy: 'Author',
latestVersionMainDocPermalink: undefined,
});
});
@ -327,6 +329,7 @@ describe('versioned site', () => {
expect(dataA).toEqual({
id: 'foo/bar',
unversionedId: 'foo/bar',
isDocsHomePage: false,
permalink: '/docs/next/foo/barSlug',
source: path.join('@site', routeBasePath, sourceA),
@ -336,6 +339,7 @@ describe('versioned site', () => {
});
expect(dataB).toEqual({
id: 'hello',
unversionedId: 'hello',
isDocsHomePage: false,
permalink: '/docs/next/hello',
source: path.join('@site', routeBasePath, sourceB),
@ -387,6 +391,7 @@ describe('versioned site', () => {
expect(dataA).toEqual({
id: 'version-1.0.0/foo/bar',
unversionedId: 'foo/bar',
isDocsHomePage: false,
permalink: '/docs/1.0.0/foo/barSlug',
source: path.join('@site', path.relative(siteDir, versionedDir), sourceA),
@ -396,6 +401,7 @@ describe('versioned site', () => {
});
expect(dataB).toEqual({
id: 'version-1.0.0/hello',
unversionedId: 'hello',
isDocsHomePage: false,
permalink: '/docs/1.0.0/hello',
source: path.join('@site', path.relative(siteDir, versionedDir), sourceB),
@ -405,6 +411,7 @@ describe('versioned site', () => {
});
expect(dataC).toEqual({
id: 'version-1.0.1/foo/bar',
unversionedId: 'foo/bar',
isDocsHomePage: false,
permalink: '/docs/foo/bar',
source: path.join('@site', path.relative(siteDir, versionedDir), sourceC),
@ -414,6 +421,7 @@ describe('versioned site', () => {
});
expect(dataD).toEqual({
id: 'version-1.0.1/hello',
unversionedId: 'hello',
isDocsHomePage: false,
permalink: '/docs/hello',
source: path.join('@site', path.relative(siteDir, versionedDir), sourceD),

View file

@ -0,0 +1,144 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {matchPath} from '@docusaurus/router';
import {GlobalPluginData, GlobalVersion, GlobalDoc} from '../types';
// This code is not part of the api surface, not in ./theme on purpose
// Short/convenient type aliases
type Version = GlobalVersion;
type Doc = GlobalDoc;
export type ActivePlugin = {
pluginId: string;
pluginData: GlobalPluginData;
};
// get the data of the plugin that is currently "active"
// ie the docs of that plugin are currently browsed
// it is useful to support multiple docs plugin instances
export const getActivePlugin = (
allPluginDatas: Record<string, GlobalPluginData>,
pathname: string,
): ActivePlugin | undefined => {
const activeEntry = Object.entries(allPluginDatas).find(
([_id, pluginData]) => {
return !!matchPath(pathname, {
path: `/${pluginData.path}`,
exact: false,
strict: false,
});
},
);
return activeEntry
? {pluginId: activeEntry[0], pluginData: activeEntry[1]}
: undefined;
};
export type ActiveDocContext = {
activeVersion?: Version;
activeDoc?: Doc;
alternateDocVersions: Record<string, Doc>;
};
export const getLatestVersion = (data: GlobalPluginData): Version => {
return data.versions.find(
(version) => version.name === data.latestVersionName,
)!;
};
// Note: return undefined on doc-unrelated pages,
// because there's no version currently considered as active
export const getActiveVersion = (
data: GlobalPluginData,
pathname: string,
): Version | undefined => {
const lastVersion = getLatestVersion(data);
// Last version is a route like /docs/*,
// we need to try to match it last or it would match /docs/version-1.0/* as well
const orderedVersionsMetadata = [
...data.versions.filter((version) => version !== lastVersion),
lastVersion,
];
return orderedVersionsMetadata.find((version) => {
return !!matchPath(pathname, {
path: version.path,
exact: false,
strict: false,
});
});
};
export const getActiveDocContext = (
data: GlobalPluginData,
pathname: string,
): ActiveDocContext => {
const activeVersion = getActiveVersion(data, pathname);
const activeDoc = activeVersion?.docs.find(
(doc) =>
!!matchPath(pathname, {
path: doc.path,
exact: true,
strict: false,
}),
);
function getAlternateVersionDocs(
docId: string,
): ActiveDocContext['alternateDocVersions'] {
const result: ActiveDocContext['alternateDocVersions'] = {};
data.versions.forEach((version) => {
version.docs.forEach((doc) => {
if (doc.id === docId) {
result[version.name!] = doc;
}
});
});
return result;
}
const alternateVersionDocs = activeDoc
? getAlternateVersionDocs(activeDoc.id)
: {};
return {
activeVersion,
activeDoc,
alternateDocVersions: alternateVersionDocs,
};
};
export type DocVersionSuggestions = {
// suggest the same doc, in latest version (if exist)
latestDocSuggestion?: GlobalDoc;
// suggest the latest version
latestVersionSuggestion?: GlobalVersion;
};
export const getDocVersionSuggestions = (
data: GlobalPluginData,
pathname: string,
): DocVersionSuggestions => {
const latestVersion = getLatestVersion(data);
const activeDocContext = getActiveDocContext(data, pathname);
// We only suggest another doc/version if user is not using the latest version
const isNotOnLatestVersion = activeDocContext.activeVersion !== latestVersion;
const latestDocSuggestion: GlobalDoc | undefined = isNotOnLatestVersion
? activeDocContext?.alternateDocVersions[latestVersion.name!]
: undefined;
const latestVersionSuggestion = isNotOnLatestVersion
? latestVersion
: undefined;
return {latestDocSuggestion, latestVersionSuggestion};
};

View file

@ -8,6 +8,7 @@
import groupBy from 'lodash.groupby';
import pick from 'lodash.pick';
import pickBy from 'lodash.pickby';
import sortBy from 'lodash.sortby';
import globby from 'globby';
import fs from 'fs-extra';
import path from 'path';
@ -49,6 +50,10 @@ import {
VersionToSidebars,
SidebarItem,
DocsSidebarItem,
GlobalPluginData,
DocsVersion,
GlobalVersion,
GlobalDoc,
} from './types';
import {Configuration} from 'webpack';
import {docsVersion} from './version';
@ -56,22 +61,6 @@ import {VERSIONS_JSON_FILE} from './constants';
import {PluginOptionSchema} from './pluginOptionSchema';
import {ValidationError} from '@hapi/joi';
function getFirstDocLinkOfSidebar(
sidebarItems: DocsSidebarItem[],
): string | null {
for (const sidebarItem of sidebarItems) {
if (sidebarItem.type === 'category') {
const url = getFirstDocLinkOfSidebar(sidebarItem.items);
if (url) {
return url;
}
} else {
return sidebarItem.href;
}
}
return null;
}
export default function pluginContentDocs(
context: LoadContext,
options: PluginOptions,
@ -92,6 +81,7 @@ export default function pluginContentDocs(
const dataDir = path.join(
generatedFilesDir,
'docusaurus-plugin-content-docs',
// options.id ?? 'default', // TODO support multi-instance
);
// Versioning.
@ -329,11 +319,23 @@ Available document ids=
}
const {docLayoutComponent, docItemComponent, routeBasePath} = options;
const {addRoute, createData} = actions;
const {addRoute, createData, setGlobalData} = actions;
const pluginInstanceGlobalData: GlobalPluginData = {
path: options.path,
latestVersionName: versioning.latestVersion,
// Initialized empty, will be mutated
versions: [],
};
setGlobalData<GlobalPluginData>(pluginInstanceGlobalData);
const aliasedSource = (source: string) =>
`~docs/${path.relative(dataDir, source)}`;
const createDocsBaseMetadata = (version?: string): DocsBaseMetadata => {
const createDocsBaseMetadata = (
version: DocsVersion,
): DocsBaseMetadata => {
const {docsSidebars, permalinkToSidebar, versionToSidebars} = content;
const neededSidebars: Set<string> =
versionToSidebars[version!] || new Set();
@ -377,13 +379,19 @@ Available document ids=
return routes.sort((a, b) => a.path.localeCompare(b.path));
};
// We want latest version route to have lower priority
// Otherwise `/docs/next/foo` would match
// `/docs/:route` instead of `/docs/next/:route`.
const getVersionRoutePriority = (version: DocsVersion) =>
version === versioning.latestVersion ? -1 : undefined;
// This is the base route of the document root (for a doc given version)
// (/docs, /docs/next, /docs/1.0 etc...)
// The component applies the layout and renders the appropriate doc
const addBaseRoute = async (
const addVersionRoute = async (
docsBasePath: string,
docsBaseMetadata: DocsBaseMetadata,
routes: RouteConfig[],
docs: Metadata[],
priority?: number,
) => {
const docsBaseMetadataPath = await createData(
@ -391,18 +399,38 @@ Available document ids=
JSON.stringify(docsBaseMetadata, null, 2),
);
const docsRoutes = await genRoutes(docs);
const mainDoc: Metadata =
docs.find((doc) => doc.unversionedId === options.homePageId) ??
docs[0];
const toGlobalDataDoc = (doc: Metadata): GlobalDoc => ({
id: doc.unversionedId,
path: doc.permalink,
});
pluginInstanceGlobalData.versions.push({
name: docsBaseMetadata.version,
path: docsBasePath,
mainDocId: mainDoc.unversionedId,
docs: docs
.map(toGlobalDataDoc)
// stable ordering, useful for tests
.sort((a, b) => a.id.localeCompare(b.id)),
});
addRoute({
path: docsBasePath,
exact: false, // allow matching /docs/* as well
component: docLayoutComponent, // main docs component (DocPage)
routes, // subroute for each doc
routes: docsRoutes, // subroute for each doc
modules: {
docsMetadata: aliasedSource(docsBaseMetadataPath),
},
priority,
});
};
// If versioning is enabled, we cleverly chunk the generated routes
// to be by version and pick only needed base metadata.
if (versioning.enabled) {
@ -410,27 +438,10 @@ Available document ids=
Object.values(content.docsMetadata),
'version',
);
const rootUrl =
options.homePageId && content.docsMetadata[options.homePageId]
? normalizeUrl([baseUrl, routeBasePath])
: getFirstDocLinkOfSidebar(
content.docsSidebars[
`version-${versioning.latestVersion}/docs`
],
);
if (!rootUrl) {
throw new Error('Bad sidebars file. No document linked');
}
Object.values(content.docsMetadata).forEach((docMetadata) => {
if (docMetadata.version !== versioning.latestVersion) {
docMetadata.latestVersionMainDocPermalink = rootUrl;
}
});
await Promise.all(
Object.keys(docsMetadataByVersion).map(async (version) => {
const routes: RouteConfig[] = await genRoutes(
docsMetadataByVersion[version],
);
const docsMetadata = docsMetadataByVersion[version];
const isLatestVersion = version === versioning.latestVersion;
const docsBaseRoute = normalizeUrl([
@ -440,23 +451,29 @@ Available document ids=
]);
const docsBaseMetadata = createDocsBaseMetadata(version);
return addBaseRoute(
await addVersionRoute(
docsBaseRoute,
docsBaseMetadata,
routes,
// We want latest version route config to be placed last in the
// generated routeconfig. Otherwise, `/docs/next/foo` will match
// `/docs/:route` instead of `/docs/next/:route`.
isLatestVersion ? -1 : undefined,
docsMetadata,
getVersionRoutePriority(version),
);
}),
);
} else {
const routes = await genRoutes(Object.values(content.docsMetadata));
const docsBaseMetadata = createDocsBaseMetadata();
const docsMetadata = Object.values(content.docsMetadata);
const docsBaseMetadata = createDocsBaseMetadata(null);
const docsBaseRoute = normalizeUrl([baseUrl, routeBasePath]);
await addBaseRoute(docsBaseRoute, docsBaseMetadata, routes);
await addVersionRoute(docsBaseRoute, docsBaseMetadata, docsMetadata);
}
// ensure version ordering on the global data (latest first)
pluginInstanceGlobalData.versions = sortBy(
pluginInstanceGlobalData.versions,
(versionMetadata: GlobalVersion) => {
const orderedVersionNames = ['next', ...versions];
return orderedVersionNames.indexOf(versionMetadata.name!);
},
);
},
async routesLoaded(routes) {

View file

@ -123,9 +123,9 @@ export default async function processMetadata({
throw new Error('Document id cannot include "/".');
}
const id = dirName !== '.' ? `${dirName}/${baseID}` : baseID;
const idWithoutVersion = version ? removeVersionPrefix(id, version) : id;
const unversionedId = version ? removeVersionPrefix(id, version) : id;
const isDocsHomePage = idWithoutVersion === homePageId;
const isDocsHomePage = unversionedId === homePageId;
if (frontMatter.slug && isDocsHomePage) {
throw new Error(
`The docs homepage (homePageId=${homePageId}) is not allowed to have a frontmatter slug=${frontMatter.slug} => you have to chooser either homePageId or slug, not both`,
@ -169,6 +169,7 @@ export default async function processMetadata({
// Adding properties to object after instantiation will cause hidden
// class transitions.
const metadata: MetadataRaw = {
unversionedId,
id,
isDocsHomePage,
title,

View file

@ -0,0 +1,65 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {useLocation} from '@docusaurus/router';
import {
useAllPluginInstancesData,
usePluginData,
} from '@docusaurus/useGlobalData';
import {GlobalPluginData, GlobalVersion} from '../../types';
import {
getActivePlugin,
getLatestVersion,
getActiveVersion,
getActiveDocContext,
getDocVersionSuggestions,
} from '../../client/docsClientUtils';
const useAllDocsData = (): Record<string, GlobalPluginData> =>
useAllPluginInstancesData('docusaurus-plugin-content-docs');
const useDocsData = (pluginId: string | undefined) =>
usePluginData('docusaurus-plugin-content-docs', pluginId) as GlobalPluginData;
export const useActivePlugin = () => {
const data = useAllDocsData();
const {pathname} = useLocation();
return getActivePlugin(data, pathname);
};
// versions are returned ordered (most recent first)
export const useVersions = (pluginId: string | undefined): GlobalVersion[] => {
const data = useDocsData(pluginId);
return data.versions;
};
export const useLatestVersion = (pluginId: string | undefined) => {
const data = useDocsData(pluginId);
return getLatestVersion(data);
};
// Note: return undefined on doc-unrelated pages,
// because there's no version currently considered as active
export const useActiveVersion = (pluginId: string | undefined) => {
const data = useDocsData(pluginId);
const {pathname} = useLocation();
return getActiveVersion(data, pathname);
};
export const useActiveDocContext = (pluginId: string | undefined) => {
const data = useDocsData(pluginId);
const {pathname} = useLocation();
return getActiveDocContext(data, pathname);
};
// Useful to say "hey, you are not on the latest docs version, please switch"
export const useDocVersionSuggestions = (pluginId: string | undefined) => {
const data = useDocsData(pluginId);
const {pathname} = useLocation();
return getDocVersionSuggestions(data, pathname);
};

View file

@ -5,6 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/
// eslint-disable-next-line spaced-comment
/// <reference types="@docusaurus/module-type-aliases" />
export type DocsVersion = string | null; // null = unversioned sites
export interface MetadataOptions {
routeBasePath: string;
homePageId?: string;
@ -19,6 +24,7 @@ export interface PathOptions {
}
export interface PluginOptions extends MetadataOptions, PathOptions {
id?: string;
include: string[];
docLayoutComponent: string;
docItemComponent: string;
@ -112,6 +118,7 @@ export interface LastUpdateData {
}
export interface MetadataRaw extends LastUpdateData {
unversionedId: string;
id: string;
isDocsHomePage: boolean;
title: string;
@ -121,7 +128,6 @@ export interface MetadataRaw extends LastUpdateData {
sidebar_label?: string;
editUrl?: string;
version?: string;
latestVersionMainDocPermalink?: string;
}
export interface Paginator {
@ -167,7 +173,7 @@ export type DocsBaseMetadata = Pick<
LoadedContent,
'docsSidebars' | 'permalinkToSidebar'
> & {
version?: string;
version: string | null;
};
export type VersioningEnv = {
@ -182,3 +188,21 @@ export interface Env {
versioning: VersioningEnv;
// TODO: translation
}
export type GlobalDoc = {
id: string;
path: string;
};
export type GlobalVersion = {
name: DocsVersion;
path: string;
mainDocId: string; // home doc (if docs homepage configured), or first doc
docs: GlobalDoc[];
};
export type GlobalPluginData = {
path: string;
latestVersionName: DocsVersion;
versions: GlobalVersion[];
};