mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-29 10:17:55 +02:00
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:
parent
a51a56ec42
commit
15e73daae7
53 changed files with 1954 additions and 531 deletions
|
@ -78,6 +78,7 @@ module.exports = {
|
|||
'@typescript-eslint/no-inferrable-types': OFF,
|
||||
'import/first': OFF,
|
||||
'import/order': OFF,
|
||||
'import/prefer-default-export': OFF,
|
||||
'lines-between-class-members': OFF,
|
||||
'no-use-before-define': [
|
||||
ERROR,
|
||||
|
|
|
@ -28,4 +28,7 @@ module.exports = {
|
|||
'^.+\\.[jt]sx?$': 'babel-jest',
|
||||
},
|
||||
setupFiles: ['./jest/stylelint-rule-test.js'],
|
||||
moduleNameMapper: {
|
||||
'@docusaurus/router': 'react-router-dom',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
"@types/lodash.kebabcase": "^4.1.6",
|
||||
"@types/lodash.pick": "^4.4.6",
|
||||
"@types/lodash.pickby": "^4.6.6",
|
||||
"@types/lodash.sortby": "^4.6.6",
|
||||
"@types/node": "^13.11.0",
|
||||
"@types/prismjs": "^1.16.1",
|
||||
"@types/react": "^16.9.38",
|
||||
|
|
|
@ -13,7 +13,7 @@ module.exports = {
|
|||
alt: 'My Site Logo',
|
||||
src: 'img/logo.svg',
|
||||
},
|
||||
links: [
|
||||
items: [
|
||||
{
|
||||
to: 'docs/',
|
||||
activeBasePath: 'docs',
|
||||
|
|
|
@ -13,7 +13,7 @@ module.exports = {
|
|||
alt: 'My Site Logo',
|
||||
src: 'img/logo.svg',
|
||||
},
|
||||
links: [
|
||||
items: [
|
||||
{
|
||||
to: 'docs/',
|
||||
activeBasePath: 'docs',
|
||||
|
|
|
@ -22,7 +22,7 @@ module.exports = {
|
|||
alt: 'My Facebook Project Logo',
|
||||
src: 'img/logo.svg',
|
||||
},
|
||||
links: [
|
||||
items: [
|
||||
{
|
||||
to: 'docs/',
|
||||
activeBasePath: 'docs',
|
||||
|
|
|
@ -42,6 +42,11 @@ declare module '@generated/routesChunkNames' {
|
|||
export default routesChunkNames;
|
||||
}
|
||||
|
||||
declare module '@generated/globalData' {
|
||||
const globalData: any;
|
||||
export default globalData;
|
||||
}
|
||||
|
||||
declare module '@theme/*';
|
||||
|
||||
declare module '@theme-original/*';
|
||||
|
|
|
@ -50,6 +50,7 @@ export default function pluginContentBlog(
|
|||
const dataDir = path.join(
|
||||
generatedFilesDir,
|
||||
'docusaurus-plugin-content-blog',
|
||||
// options.id ?? 'default', // TODO support multi-instance
|
||||
);
|
||||
let blogPosts: BlogPost[] = [];
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ export interface DateLink {
|
|||
export type FeedType = 'rss' | 'atom';
|
||||
|
||||
export interface PluginOptions {
|
||||
id?: string;
|
||||
path: string;
|
||||
routeBasePath: string;
|
||||
include: string[];
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^2.0.0-alpha.58",
|
||||
"commander": "^5.0.0",
|
||||
"picomatch": "^2.1.1",
|
||||
"@types/hapi__joi": "^17.1.2"
|
||||
|
@ -30,6 +31,7 @@
|
|||
"lodash.groupby": "^4.6.0",
|
||||
"lodash.pick": "^4.4.0",
|
||||
"lodash.pickby": "^4.6.0",
|
||||
"lodash.sortby": "^4.6.0",
|
||||
"remark-admonitions": "^1.2.1",
|
||||
"shelljs": "^0.8.4"
|
||||
},
|
||||
|
|
|
@ -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 [
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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};
|
||||
};
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -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[];
|
||||
};
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
"description": "Debug plugin for Docusaurus",
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc"
|
||||
"build": "tsc",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
const path = require('path');
|
||||
const Module = require('module');
|
||||
const Joi = require('@hapi/joi');
|
||||
const ThemeConfigSchema = require('./themeConfigSchema');
|
||||
|
||||
const createRequire = Module.createRequire || Module.createRequireFromPath;
|
||||
const requireFromDocusaurusCore = createRequire(
|
||||
|
@ -120,88 +120,6 @@ module.exports = function (context, options) {
|
|||
};
|
||||
};
|
||||
|
||||
const NavbarLinkSchema = Joi.object({
|
||||
items: Joi.array().optional().items(Joi.link('#navbarLinkSchema')),
|
||||
to: Joi.string(),
|
||||
href: Joi.string().uri(),
|
||||
prependBaseUrlToHref: Joi.bool().default(true),
|
||||
label: Joi.string(),
|
||||
position: Joi.string().equal('left', 'right').default('left'),
|
||||
activeBasePath: Joi.string(),
|
||||
activeBaseRegex: Joi.string(),
|
||||
className: Joi.string(),
|
||||
'aria-label': Joi.string(),
|
||||
})
|
||||
.xor('href', 'to')
|
||||
.id('navbarLinkSchema');
|
||||
|
||||
const ColorModeSchema = Joi.object({
|
||||
defaultMode: Joi.string().equal('dark', 'light').default('light'),
|
||||
disableSwitch: Joi.bool().default(false),
|
||||
respectPrefersColorScheme: Joi.bool().default(false),
|
||||
}).default({
|
||||
defaultMode: 'light',
|
||||
disableSwitch: false,
|
||||
respectPrefersColorScheme: false,
|
||||
});
|
||||
|
||||
const ThemeConfigSchema = Joi.object({
|
||||
disableDarkMode: Joi.any().forbidden(false).messages({
|
||||
'any.unknown':
|
||||
'disableDarkMode theme config is deprecated. Please use the new colorMode attribute. You likely want: config.themeConfig.colorMode.disableSwitch = true',
|
||||
}),
|
||||
defaultDarkMode: Joi.any().forbidden(false).messages({
|
||||
'any.unknown':
|
||||
'defaultDarkMode theme config is deprecated. Please use the new colorMode attribute. You likely want: config.themeConfig.colorMode.defaultMode = "dark"',
|
||||
}),
|
||||
colorMode: ColorModeSchema,
|
||||
image: Joi.string(),
|
||||
announcementBar: Joi.object({
|
||||
id: Joi.string(),
|
||||
content: Joi.string(),
|
||||
backgroundColor: Joi.string().default('#fff'),
|
||||
textColor: Joi.string().default('#000'),
|
||||
}).optional(),
|
||||
navbar: Joi.object({
|
||||
hideOnScroll: Joi.bool().default(false),
|
||||
links: Joi.array().items(NavbarLinkSchema),
|
||||
title: Joi.string().required(),
|
||||
logo: Joi.object({
|
||||
alt: Joi.string(),
|
||||
src: Joi.string().required(),
|
||||
srcDark: Joi.string(),
|
||||
href: Joi.string(),
|
||||
target: Joi.string(),
|
||||
}),
|
||||
}),
|
||||
footer: Joi.object({
|
||||
style: Joi.string().equal('dark', 'light').default('light'),
|
||||
logo: Joi.object({
|
||||
alt: Joi.string(),
|
||||
src: Joi.string(),
|
||||
href: Joi.string(),
|
||||
}),
|
||||
copyright: Joi.string(),
|
||||
links: Joi.array().items(
|
||||
Joi.object({
|
||||
title: Joi.string().required(),
|
||||
items: Joi.array().items(
|
||||
Joi.object({
|
||||
to: Joi.string(),
|
||||
href: Joi.string().uri(),
|
||||
html: Joi.string(),
|
||||
label: Joi.string(),
|
||||
})
|
||||
.xor('to', 'href', 'html')
|
||||
.with('to', 'label')
|
||||
.with('href', 'label')
|
||||
.nand('html', 'label'),
|
||||
),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
module.exports.validateThemeConfig = ({validate, themeConfig}) => {
|
||||
return validate(ThemeConfigSchema, themeConfig);
|
||||
};
|
||||
|
|
|
@ -12,7 +12,7 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
import DocPaginator from '@theme/DocPaginator';
|
||||
import useTOCHighlight from '@theme/hooks/useTOCHighlight';
|
||||
import Link from '@docusaurus/Link';
|
||||
import DocVersionSuggestions from '@theme/DocVersionSuggestions';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import styles from './styles.module.css';
|
||||
|
@ -71,7 +71,6 @@ function DocItem(props): JSX.Element {
|
|||
lastUpdatedAt,
|
||||
lastUpdatedBy,
|
||||
version,
|
||||
latestVersionMainDocPermalink,
|
||||
} = metadata;
|
||||
const {
|
||||
frontMatter: {
|
||||
|
@ -84,7 +83,6 @@ function DocItem(props): JSX.Element {
|
|||
|
||||
const metaTitle = title ? `${title} | ${siteTitle}` : siteTitle;
|
||||
const metaImageUrl = useBaseUrl(metaImage, {absolute: true});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
|
@ -112,33 +110,7 @@ function DocItem(props): JSX.Element {
|
|||
className={clsx('col', {
|
||||
[styles.docItemCol]: !hideTableOfContents,
|
||||
})}>
|
||||
{latestVersionMainDocPermalink && (
|
||||
<div
|
||||
className="alert alert--warning margin-bottom--md"
|
||||
role="alert">
|
||||
{version === 'next' ? (
|
||||
<div>
|
||||
This is unreleased documentation for {siteTitle}{' '}
|
||||
<strong>{version}</strong> version.
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
This is archived documentation for {siteTitle}{' '}
|
||||
<strong>v{version}</strong>, which is no longer actively
|
||||
maintained.
|
||||
</div>
|
||||
)}
|
||||
<div className="margin-top--md">
|
||||
For up-to-date documentation, see the{' '}
|
||||
<strong>
|
||||
<Link to={latestVersionMainDocPermalink}>
|
||||
latest version
|
||||
</Link>
|
||||
</strong>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DocVersionSuggestions />
|
||||
<div className={styles.docItemContainer}>
|
||||
<article>
|
||||
{version && (
|
||||
|
|
|
@ -18,47 +18,56 @@ import {matchPath} from '@docusaurus/router';
|
|||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
function DocPage(props): JSX.Element {
|
||||
const {route: baseRoute, docsMetadata, location} = props;
|
||||
// case-sensitive route such as it is defined in the sidebar
|
||||
const currentRoute =
|
||||
baseRoute.routes.find((route) => {
|
||||
return matchPath(location.pathname, route);
|
||||
}) || {};
|
||||
function DocPageContent({
|
||||
currentDocRoute,
|
||||
docsMetadata,
|
||||
children,
|
||||
}): JSX.Element {
|
||||
const {siteConfig, isClient} = useDocusaurusContext();
|
||||
const {permalinkToSidebar, docsSidebars, version} = docsMetadata;
|
||||
const sidebar = permalinkToSidebar[currentRoute.path];
|
||||
const {
|
||||
siteConfig: {themeConfig = {}} = {},
|
||||
isClient,
|
||||
} = useDocusaurusContext();
|
||||
|
||||
const {sidebarCollapsible = true} = themeConfig;
|
||||
|
||||
if (Object.keys(currentRoute).length === 0) {
|
||||
return <NotFound {...props} />;
|
||||
}
|
||||
|
||||
const sidebarName = permalinkToSidebar[currentDocRoute.path];
|
||||
const sidebar = docsSidebars[sidebarName];
|
||||
return (
|
||||
<Layout version={version} key={isClient}>
|
||||
<div className={styles.docPage}>
|
||||
{sidebar && (
|
||||
<div className={styles.docSidebarContainer} role="complementary">
|
||||
<DocSidebar
|
||||
docsSidebars={docsSidebars}
|
||||
path={currentRoute.path}
|
||||
sidebar={sidebar}
|
||||
sidebarCollapsible={sidebarCollapsible}
|
||||
path={currentDocRoute.path}
|
||||
sidebarCollapsible={
|
||||
siteConfig.themeConfig?.sidebarCollapsible ?? true
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<main className={styles.docMainContainer}>
|
||||
<MDXProvider components={MDXComponents}>
|
||||
{renderRoutes(baseRoute.routes)}
|
||||
</MDXProvider>
|
||||
<MDXProvider components={MDXComponents}>{children}</MDXProvider>
|
||||
</main>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
function DocPage(props) {
|
||||
const {
|
||||
route: {routes: docRoutes},
|
||||
docsMetadata,
|
||||
location,
|
||||
} = props;
|
||||
const currentDocRoute = docRoutes.find((docRoute) =>
|
||||
matchPath(location.pathname, docRoute),
|
||||
);
|
||||
if (!currentDocRoute) {
|
||||
return <NotFound {...props} />;
|
||||
}
|
||||
return (
|
||||
<DocPageContent
|
||||
currentDocRoute={currentDocRoute}
|
||||
docsMetadata={docsMetadata}>
|
||||
{renderRoutes(docRoutes)}
|
||||
</DocPageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default DocPage;
|
||||
|
|
|
@ -163,7 +163,11 @@ function DocSidebarItem(props) {
|
|||
}
|
||||
}
|
||||
|
||||
function DocSidebar(props): JSX.Element | null {
|
||||
function DocSidebar({
|
||||
path,
|
||||
sidebar,
|
||||
sidebarCollapsible = true,
|
||||
}): JSX.Element | null {
|
||||
const [showResponsiveSidebar, setShowResponsiveSidebar] = useState(false);
|
||||
const {
|
||||
siteConfig: {
|
||||
|
@ -175,13 +179,6 @@ function DocSidebar(props): JSX.Element | null {
|
|||
const {isAnnouncementBarClosed} = useUserPreferencesContext();
|
||||
const {scrollY} = useScrollPosition();
|
||||
|
||||
const {
|
||||
docsSidebars,
|
||||
path,
|
||||
sidebar: currentSidebar,
|
||||
sidebarCollapsible,
|
||||
} = props;
|
||||
|
||||
useLockBodyScroll(showResponsiveSidebar);
|
||||
const windowSize = useWindowSize();
|
||||
|
||||
|
@ -191,18 +188,6 @@ function DocSidebar(props): JSX.Element | null {
|
|||
}
|
||||
}, [windowSize]);
|
||||
|
||||
if (!currentSidebar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sidebarData = docsSidebars[currentSidebar];
|
||||
|
||||
if (!sidebarData) {
|
||||
throw new Error(
|
||||
`Cannot find the sidebar "${currentSidebar}" in the sidebar config!`,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.sidebar, {
|
||||
|
@ -264,7 +249,7 @@ function DocSidebar(props): JSX.Element | null {
|
|||
)}
|
||||
</button>
|
||||
<ul className="menu__list">
|
||||
{sidebarData.map((item) => (
|
||||
{sidebar.map((item) => (
|
||||
<DocSidebarItem
|
||||
key={item.label}
|
||||
item={item}
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* 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 React from 'react';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import Link from '@docusaurus/Link';
|
||||
import {
|
||||
useActivePlugin,
|
||||
useActiveVersion,
|
||||
useDocVersionSuggestions,
|
||||
} from '@theme/hooks/useDocs';
|
||||
|
||||
const useMandatoryActiveDocsPluginId = () => {
|
||||
const activePlugin = useActivePlugin();
|
||||
if (!activePlugin) {
|
||||
throw new Error(
|
||||
'DocVersionCallout is only supposed to be used on docs-related routes',
|
||||
);
|
||||
}
|
||||
return activePlugin.pluginId;
|
||||
};
|
||||
|
||||
const getVersionMainDoc = (version) =>
|
||||
version.docs.find((doc) => doc.id === version.mainDocId);
|
||||
|
||||
function DocVersionSuggestions(): JSX.Element {
|
||||
const {
|
||||
siteConfig: {title: siteTitle},
|
||||
} = useDocusaurusContext();
|
||||
const pluginId = useMandatoryActiveDocsPluginId();
|
||||
const activeVersion = useActiveVersion(pluginId);
|
||||
const {
|
||||
latestDocSuggestion,
|
||||
latestVersionSuggestion,
|
||||
} = useDocVersionSuggestions(pluginId);
|
||||
|
||||
// No suggestion to be made
|
||||
if (!latestVersionSuggestion) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const activeVersionName = activeVersion.name;
|
||||
|
||||
// try to link to same doc in latest version (not always possible)
|
||||
// fallback to main doc of latest version
|
||||
const suggestedDoc =
|
||||
latestDocSuggestion ?? getVersionMainDoc(latestVersionSuggestion);
|
||||
|
||||
return (
|
||||
<div className="alert alert--warning margin-bottom--md" role="alert">
|
||||
{activeVersionName === 'next' ? (
|
||||
<div>
|
||||
This is unreleased documentation for {siteTitle}{' '}
|
||||
<strong>{activeVersionName}</strong> version.
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
This is documentation for {siteTitle}{' '}
|
||||
<strong>v{activeVersionName}</strong>, which is no longer actively
|
||||
maintained.
|
||||
</div>
|
||||
)}
|
||||
<div className="margin-top--md">
|
||||
For up-to-date documentation, see the{' '}
|
||||
<strong>
|
||||
<Link to={suggestedDoc.path}>latest version</Link>
|
||||
</strong>{' '}
|
||||
({latestVersionSuggestion.name}).
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DocVersionSuggestions;
|
|
@ -18,6 +18,14 @@ import Footer from '@theme/Footer';
|
|||
|
||||
import './styles.css';
|
||||
|
||||
function Providers({children}) {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<UserPreferencesProvider>{children}</UserPreferencesProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
title?: string;
|
||||
|
@ -53,42 +61,36 @@ function Layout(props: Props): JSX.Element {
|
|||
const faviconUrl = useBaseUrl(favicon);
|
||||
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<UserPreferencesProvider>
|
||||
<Head>
|
||||
{/* TODO: Do not assume that it is in english language */}
|
||||
<html lang="en" />
|
||||
<Providers>
|
||||
<Head>
|
||||
{/* TODO: Do not assume that it is in english language */}
|
||||
<html lang="en" />
|
||||
|
||||
{metaTitle && <title>{metaTitle}</title>}
|
||||
{metaTitle && <meta property="og:title" content={metaTitle} />}
|
||||
{favicon && <link rel="shortcut icon" href={faviconUrl} />}
|
||||
{description && <meta name="description" content={description} />}
|
||||
{description && (
|
||||
<meta property="og:description" content={description} />
|
||||
)}
|
||||
{version && <meta name="docsearch:version" content={version} />}
|
||||
{keywords && keywords.length && (
|
||||
<meta name="keywords" content={keywords.join(',')} />
|
||||
)}
|
||||
{metaImage && <meta property="og:image" content={metaImageUrl} />}
|
||||
{metaImage && (
|
||||
<meta property="twitter:image" content={metaImageUrl} />
|
||||
)}
|
||||
{metaImage && (
|
||||
<meta name="twitter:image:alt" content={`Image for ${metaTitle}`} />
|
||||
)}
|
||||
{permalink && (
|
||||
<meta property="og:url" content={siteUrl + permalink} />
|
||||
)}
|
||||
{permalink && <link rel="canonical" href={siteUrl + permalink} />}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
</Head>
|
||||
<AnnouncementBar />
|
||||
<Navbar />
|
||||
<div className="main-wrapper">{children}</div>
|
||||
{!noFooter && <Footer />}
|
||||
</UserPreferencesProvider>
|
||||
</ThemeProvider>
|
||||
{metaTitle && <title>{metaTitle}</title>}
|
||||
{metaTitle && <meta property="og:title" content={metaTitle} />}
|
||||
{favicon && <link rel="shortcut icon" href={faviconUrl} />}
|
||||
{description && <meta name="description" content={description} />}
|
||||
{description && (
|
||||
<meta property="og:description" content={description} />
|
||||
)}
|
||||
{version && <meta name="docsearch:version" content={version} />}
|
||||
{keywords && keywords.length && (
|
||||
<meta name="keywords" content={keywords.join(',')} />
|
||||
)}
|
||||
{metaImage && <meta property="og:image" content={metaImageUrl} />}
|
||||
{metaImage && <meta property="twitter:image" content={metaImageUrl} />}
|
||||
{metaImage && (
|
||||
<meta name="twitter:image:alt" content={`Image for ${metaTitle}`} />
|
||||
)}
|
||||
{permalink && <meta property="og:url" content={siteUrl + permalink} />}
|
||||
{permalink && <link rel="canonical" href={siteUrl + permalink} />}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
</Head>
|
||||
<AnnouncementBar />
|
||||
<Navbar />
|
||||
<div className="main-wrapper">{children}</div>
|
||||
{!noFooter && <Footer />}
|
||||
</Providers>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,11 +5,10 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {useCallback, useState, useEffect, ComponentProps} from 'react';
|
||||
import React, {useCallback, useState, useEffect} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import Link from '@docusaurus/Link';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
import SearchBar from '@theme/SearchBar';
|
||||
import Toggle from '@theme/Toggle';
|
||||
|
@ -20,163 +19,23 @@ import useWindowSize, {windowSizes} from '@theme/hooks/useWindowSize';
|
|||
import useLogo from '@theme/hooks/useLogo';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
import NavbarItem from '@theme/NavbarItem';
|
||||
|
||||
// retrocompatible with v1
|
||||
const DefaultNavItemPosition = 'right';
|
||||
|
||||
function NavLink({
|
||||
activeBasePath,
|
||||
activeBaseRegex,
|
||||
to,
|
||||
href,
|
||||
label,
|
||||
activeClassName = 'navbar__link--active',
|
||||
prependBaseUrlToHref,
|
||||
...props
|
||||
}: {
|
||||
activeBasePath?: string;
|
||||
activeBaseRegex?: string;
|
||||
to?: string;
|
||||
href?: string;
|
||||
label?: string;
|
||||
activeClassName?: string;
|
||||
prependBaseUrlToHref?: string;
|
||||
} & ComponentProps<'a'>) {
|
||||
const toUrl = useBaseUrl(to);
|
||||
const activeBaseUrl = useBaseUrl(activeBasePath);
|
||||
const normalizedHref = useBaseUrl(href, {forcePrependBaseUrl: true});
|
||||
|
||||
return (
|
||||
<Link
|
||||
{...(href
|
||||
? {
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
href: prependBaseUrlToHref ? normalizedHref : href,
|
||||
}
|
||||
: {
|
||||
isNavLink: true,
|
||||
activeClassName,
|
||||
to: toUrl,
|
||||
...(activeBasePath || activeBaseRegex
|
||||
? {
|
||||
isActive: (_match, location) =>
|
||||
activeBaseRegex
|
||||
? new RegExp(activeBaseRegex).test(location.pathname)
|
||||
: location.pathname.startsWith(activeBaseUrl),
|
||||
}
|
||||
: null),
|
||||
})}
|
||||
{...props}>
|
||||
{label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function NavItem({
|
||||
items,
|
||||
position = DefaultNavItemPosition,
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
const navLinkClassNames = (extraClassName, isDropdownItem = false) =>
|
||||
clsx(
|
||||
{
|
||||
'navbar__item navbar__link': !isDropdownItem,
|
||||
dropdown__link: isDropdownItem,
|
||||
},
|
||||
extraClassName,
|
||||
);
|
||||
|
||||
if (!items) {
|
||||
return <NavLink className={navLinkClassNames(className)} {...props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('navbar__item', 'dropdown', 'dropdown--hoverable', {
|
||||
'dropdown--left': position === 'left',
|
||||
'dropdown--right': position === 'right',
|
||||
})}>
|
||||
<NavLink
|
||||
className={navLinkClassNames(className)}
|
||||
{...props}
|
||||
onClick={(e) => e.preventDefault()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
((e.target as HTMLElement)
|
||||
.parentNode as HTMLElement).classList.toggle('dropdown--show');
|
||||
}
|
||||
}}>
|
||||
{props.label}
|
||||
</NavLink>
|
||||
<ul className="dropdown__menu">
|
||||
{items.map(({className: childItemClassName, ...childItemProps}, i) => (
|
||||
<li key={i}>
|
||||
<NavLink
|
||||
activeClassName="dropdown__link--active"
|
||||
className={navLinkClassNames(childItemClassName, true)}
|
||||
{...childItemProps}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileNavItem({items, position: _position, className, ...props}) {
|
||||
// Need to destructure position from props so that it doesn't get passed on.
|
||||
const navLinkClassNames = (extraClassName, isSubList = false) =>
|
||||
clsx(
|
||||
'menu__link',
|
||||
{
|
||||
'menu__link--sublist': isSubList,
|
||||
},
|
||||
extraClassName,
|
||||
);
|
||||
|
||||
if (!items) {
|
||||
return (
|
||||
<li className="menu__list-item">
|
||||
<NavLink className={navLinkClassNames(className)} {...props} />
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="menu__list-item">
|
||||
<NavLink className={navLinkClassNames(className, true)} {...props}>
|
||||
{props.label}
|
||||
</NavLink>
|
||||
<ul className="menu__list">
|
||||
{items.map(({className: childItemClassName, ...childItemProps}, i) => (
|
||||
<li className="menu__list-item" key={i}>
|
||||
<NavLink
|
||||
activeClassName="menu__link--active"
|
||||
className={navLinkClassNames(childItemClassName)}
|
||||
{...childItemProps}
|
||||
onClick={props.onClick}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
// If split links by left/right
|
||||
// if position is unspecified, fallback to right (as v1)
|
||||
function splitLinks(links) {
|
||||
const leftLinks = links.filter(
|
||||
(linkItem) => (linkItem.position ?? DefaultNavItemPosition) === 'left',
|
||||
function splitNavItemsByPosition(items) {
|
||||
const leftItems = items.filter(
|
||||
(item) => (item.position ?? DefaultNavItemPosition) === 'left',
|
||||
);
|
||||
const rightLinks = links.filter(
|
||||
(linkItem) => (linkItem.position ?? DefaultNavItemPosition) === 'right',
|
||||
const rightItems = items.filter(
|
||||
(item) => (item.position ?? DefaultNavItemPosition) === 'right',
|
||||
);
|
||||
return {
|
||||
leftLinks,
|
||||
rightLinks,
|
||||
leftItems,
|
||||
rightItems,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -184,7 +43,7 @@ function Navbar(): JSX.Element {
|
|||
const {
|
||||
siteConfig: {
|
||||
themeConfig: {
|
||||
navbar: {title = '', links = [], hideOnScroll = false} = {},
|
||||
navbar: {title = '', items = [], hideOnScroll = false} = {},
|
||||
colorMode: {disableSwitch: disableColorModeSwitch = false} = {},
|
||||
},
|
||||
},
|
||||
|
@ -219,7 +78,7 @@ function Navbar(): JSX.Element {
|
|||
}
|
||||
}, [windowSize]);
|
||||
|
||||
const {leftLinks, rightLinks} = splitLinks(links);
|
||||
const {leftItems, rightItems} = splitNavItemsByPosition(items);
|
||||
|
||||
return (
|
||||
<nav
|
||||
|
@ -231,7 +90,7 @@ function Navbar(): JSX.Element {
|
|||
})}>
|
||||
<div className="navbar__inner">
|
||||
<div className="navbar__items">
|
||||
{links != null && links.length !== 0 && (
|
||||
{items != null && items.length !== 0 && (
|
||||
<div
|
||||
aria-label="Navigation bar toggle"
|
||||
className="navbar__toggle"
|
||||
|
@ -275,13 +134,13 @@ function Navbar(): JSX.Element {
|
|||
</strong>
|
||||
)}
|
||||
</Link>
|
||||
{leftLinks.map((linkItem, i) => (
|
||||
<NavItem {...linkItem} key={i} />
|
||||
{leftItems.map((item, i) => (
|
||||
<NavbarItem {...item} key={i} />
|
||||
))}
|
||||
</div>
|
||||
<div className="navbar__items navbar__items--right">
|
||||
{rightLinks.map((linkItem, i) => (
|
||||
<NavItem {...linkItem} key={i} />
|
||||
{rightItems.map((item, i) => (
|
||||
<NavbarItem {...item} key={i} />
|
||||
))}
|
||||
{!disableColorModeSwitch && (
|
||||
<Toggle
|
||||
|
@ -332,8 +191,8 @@ function Navbar(): JSX.Element {
|
|||
<div className="navbar-sidebar__items">
|
||||
<div className="menu">
|
||||
<ul className="menu__list">
|
||||
{links.map((linkItem, i) => (
|
||||
<MobileNavItem {...linkItem} onClick={hideSidebar} key={i} />
|
||||
{items.map((item, i) => (
|
||||
<NavbarItem mobile {...item} onClick={hideSidebar} key={i} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
/**
|
||||
* 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 React, {ComponentProps, ComponentType} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import Link from '@docusaurus/Link';
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
function NavLink({
|
||||
activeBasePath,
|
||||
activeBaseRegex,
|
||||
to,
|
||||
href,
|
||||
label,
|
||||
activeClassName = 'navbar__link--active',
|
||||
prependBaseUrlToHref,
|
||||
...props
|
||||
}: {
|
||||
activeBasePath?: string;
|
||||
activeBaseRegex?: string;
|
||||
to?: string;
|
||||
href?: string;
|
||||
label?: string;
|
||||
activeClassName?: string;
|
||||
prependBaseUrlToHref?: string;
|
||||
} & ComponentProps<'a'>) {
|
||||
const toUrl = useBaseUrl(to);
|
||||
const activeBaseUrl = useBaseUrl(activeBasePath);
|
||||
const normalizedHref = useBaseUrl(href, {forcePrependBaseUrl: true});
|
||||
|
||||
return (
|
||||
<Link
|
||||
{...(href
|
||||
? {
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
href: prependBaseUrlToHref ? normalizedHref : href,
|
||||
}
|
||||
: {
|
||||
isNavLink: true,
|
||||
activeClassName,
|
||||
to: toUrl,
|
||||
...(activeBasePath || activeBaseRegex
|
||||
? {
|
||||
isActive: (_match, location) =>
|
||||
activeBaseRegex
|
||||
? new RegExp(activeBaseRegex).test(location.pathname)
|
||||
: location.pathname.startsWith(activeBaseUrl),
|
||||
}
|
||||
: null),
|
||||
})}
|
||||
{...props}>
|
||||
{label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function NavItemDesktop({items, position, className, ...props}) {
|
||||
const navLinkClassNames = (extraClassName, isDropdownItem = false) =>
|
||||
clsx(
|
||||
{
|
||||
'navbar__item navbar__link': !isDropdownItem,
|
||||
dropdown__link: isDropdownItem,
|
||||
},
|
||||
extraClassName,
|
||||
);
|
||||
|
||||
if (!items) {
|
||||
return <NavLink className={navLinkClassNames(className)} {...props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('navbar__item', 'dropdown', 'dropdown--hoverable', {
|
||||
'dropdown--left': position === 'left',
|
||||
'dropdown--right': position === 'right',
|
||||
})}>
|
||||
<NavLink
|
||||
className={navLinkClassNames(className)}
|
||||
{...props}
|
||||
onClick={(e) => e.preventDefault()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
((e.target as HTMLElement)
|
||||
.parentNode as HTMLElement).classList.toggle('dropdown--show');
|
||||
}
|
||||
}}>
|
||||
{props.label}
|
||||
</NavLink>
|
||||
<ul className="dropdown__menu">
|
||||
{items.map(({className: childItemClassName, ...childItemProps}, i) => (
|
||||
<li key={i}>
|
||||
<NavLink
|
||||
activeClassName="dropdown__link--active"
|
||||
className={navLinkClassNames(childItemClassName, true)}
|
||||
{...childItemProps}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NavItemMobile({items, position: _position, className, ...props}) {
|
||||
// Need to destructure position from props so that it doesn't get passed on.
|
||||
const navLinkClassNames = (extraClassName, isSubList = false) =>
|
||||
clsx(
|
||||
'menu__link',
|
||||
{
|
||||
'menu__link--sublist': isSubList,
|
||||
},
|
||||
extraClassName,
|
||||
);
|
||||
|
||||
if (!items) {
|
||||
return (
|
||||
<li className="menu__list-item">
|
||||
<NavLink className={navLinkClassNames(className)} {...props} />
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="menu__list-item">
|
||||
<NavLink className={navLinkClassNames(className, true)} {...props}>
|
||||
{props.label}
|
||||
</NavLink>
|
||||
<ul className="menu__list">
|
||||
{items.map(({className: childItemClassName, ...childItemProps}, i) => (
|
||||
<li className="menu__list-item" key={i}>
|
||||
<NavLink
|
||||
activeClassName="menu__link--active"
|
||||
className={navLinkClassNames(childItemClassName)}
|
||||
{...childItemProps}
|
||||
onClick={props.onClick}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function DefaultNavbarItem({mobile = false, ...props}) {
|
||||
const Comp: ComponentType<any> = mobile ? NavItemMobile : NavItemDesktop;
|
||||
return <Comp {...props} />;
|
||||
}
|
||||
|
||||
export default DefaultNavbarItem;
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* 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 React from 'react';
|
||||
import DefaultNavbarItem from './DefaultNavbarItem';
|
||||
import {
|
||||
useVersions,
|
||||
useLatestVersion,
|
||||
useActiveDocContext,
|
||||
} from '@theme/hooks/useDocs';
|
||||
|
||||
const versionLabel = (version) =>
|
||||
version.name === 'next' ? 'Next/Master' : version.name;
|
||||
|
||||
const getVersionMainDoc = (version) =>
|
||||
version.docs.find((doc) => doc.id === version.mainDocId);
|
||||
|
||||
export default function DocsVersionDropdownNavbarItem({
|
||||
docsPluginId,
|
||||
...props
|
||||
}) {
|
||||
const activeDocContext = useActiveDocContext(docsPluginId);
|
||||
const versions = useVersions(docsPluginId);
|
||||
const latestVersion = useLatestVersion(docsPluginId);
|
||||
|
||||
const items = versions.map((version) => {
|
||||
// We try to link to the same doc, in another version
|
||||
// When not possible, fallback to the "main doc" of the version
|
||||
const versionDoc =
|
||||
activeDocContext?.alternateDocVersions[version.name] ||
|
||||
getVersionMainDoc(version);
|
||||
return {
|
||||
isNavLink: true,
|
||||
label: versionLabel(version),
|
||||
to: versionDoc.path,
|
||||
isActive: () => version === activeDocContext?.activeVersion,
|
||||
};
|
||||
});
|
||||
|
||||
const dropdownVersion = activeDocContext.activeVersion ?? latestVersion;
|
||||
|
||||
return (
|
||||
<DefaultNavbarItem
|
||||
{...props}
|
||||
label={versionLabel(dropdownVersion)}
|
||||
to={getVersionMainDoc(dropdownVersion).path}
|
||||
items={items}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* 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 React from 'react';
|
||||
import DefaultNavbarItem from './DefaultNavbarItem';
|
||||
import {useActiveVersion, useLatestVersion} from '@theme/hooks/useDocs';
|
||||
|
||||
const getVersionMainDoc = (version) =>
|
||||
version.docs.find((doc) => doc.id === version.mainDocId);
|
||||
|
||||
export default function DocsVersionNavbarItem({
|
||||
label: staticLabel,
|
||||
to: staticTo,
|
||||
docsPluginId,
|
||||
...props
|
||||
}) {
|
||||
const activeVersion = useActiveVersion(docsPluginId);
|
||||
const latestVersion = useLatestVersion(docsPluginId);
|
||||
const version = activeVersion ?? latestVersion;
|
||||
const label = staticLabel ?? version.name;
|
||||
const path = staticTo ?? getVersionMainDoc(version).path;
|
||||
return <DefaultNavbarItem {...props} label={label} to={path} />;
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* 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 React from 'react';
|
||||
import DocsVersionNavbarItem from '@theme/NavbarItem/DocsVersionNavbarItem';
|
||||
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
|
||||
import DocsVersionDropdownNavbarItem from '@theme/NavbarItem/DocsVersionDropdownNavbarItem';
|
||||
|
||||
const NavbarItemComponents = {
|
||||
default: DefaultNavbarItem,
|
||||
docsVersion: DocsVersionNavbarItem,
|
||||
docsVersionDropdown: DocsVersionDropdownNavbarItem,
|
||||
};
|
||||
|
||||
const getNavbarItemComponent = (type: string = 'default') => {
|
||||
const NavbarItemComponent = NavbarItemComponents[type];
|
||||
if (!NavbarItemComponent) {
|
||||
throw new Error(`No NavbarItem component found for type=${type}.`);
|
||||
}
|
||||
return NavbarItemComponent;
|
||||
};
|
||||
|
||||
export default function NavbarItem({type, ...props}) {
|
||||
const NavbarItemComponent = getNavbarItemComponent(type);
|
||||
return <NavbarItemComponent {...props} />;
|
||||
}
|
168
packages/docusaurus-theme-classic/src/themeConfigSchema.js
Normal file
168
packages/docusaurus-theme-classic/src/themeConfigSchema.js
Normal file
|
@ -0,0 +1,168 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const Joi = require('@hapi/joi');
|
||||
|
||||
const NavbarItemPosition = Joi.string().equal('left', 'right').default('left');
|
||||
|
||||
const DefaultNavbarItemSchema = Joi.object({
|
||||
items: Joi.array().optional().items(Joi.link('...')),
|
||||
to: Joi.string(),
|
||||
href: Joi.string().uri(),
|
||||
prependBaseUrlToHref: Joi.bool().default(true),
|
||||
label: Joi.string(),
|
||||
position: NavbarItemPosition,
|
||||
activeBasePath: Joi.string(),
|
||||
activeBaseRegex: Joi.string(),
|
||||
className: Joi.string(),
|
||||
'aria-label': Joi.string(),
|
||||
}).xor('href', 'to');
|
||||
|
||||
const DocsVersionNavbarItemSchema = Joi.object({
|
||||
type: Joi.string().equal('docsVersion').required(),
|
||||
position: NavbarItemPosition,
|
||||
label: Joi.string(),
|
||||
to: Joi.string(),
|
||||
docsPluginId: Joi.string(),
|
||||
});
|
||||
|
||||
const DocsVersionDropdownNavbarItemSchema = Joi.object({
|
||||
type: Joi.string().equal('docsVersionDropdown').required(),
|
||||
position: NavbarItemPosition,
|
||||
docsPluginId: Joi.string(),
|
||||
});
|
||||
|
||||
// Can this be made easier? :/
|
||||
const isOfType = (type) => {
|
||||
let typeSchema = Joi.string().required();
|
||||
// because equal(undefined) is not supported :/
|
||||
if (type) {
|
||||
typeSchema = typeSchema.equal(type);
|
||||
}
|
||||
return Joi.object({
|
||||
type: typeSchema,
|
||||
})
|
||||
.unknown()
|
||||
.required();
|
||||
};
|
||||
|
||||
const NavbarItemSchema = Joi.object().when({
|
||||
switch: [
|
||||
{
|
||||
is: isOfType('docsVersion'),
|
||||
then: DocsVersionNavbarItemSchema,
|
||||
},
|
||||
{
|
||||
is: isOfType('docsVersionDropdown'),
|
||||
then: DocsVersionDropdownNavbarItemSchema,
|
||||
},
|
||||
{
|
||||
is: isOfType(undefined),
|
||||
then: Joi.forbidden().messages({
|
||||
'any.unknown': 'Bad nav item type {.type}',
|
||||
}),
|
||||
},
|
||||
],
|
||||
otherwise: DefaultNavbarItemSchema,
|
||||
});
|
||||
|
||||
/*
|
||||
const NavbarItemSchema = Joi.object({
|
||||
type: Joi.string().only(['docsVersion'])
|
||||
})
|
||||
.when(Joi.object({ type: 'docsVersion' }).unknown(), {
|
||||
then: Joi.object({ pepperoni: Joi.boolean() })
|
||||
})
|
||||
.when(Joi.object().unknown(), {
|
||||
then: Joi.object({ croutons: Joi.boolean() })
|
||||
})
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
const NavbarItemSchema = Joi.object().when('type', {
|
||||
is: Joi.valid('docsVersion'),
|
||||
then: DocsVersionNavbarItemSchema,
|
||||
otherwise: DefaultNavbarItemSchema,
|
||||
});
|
||||
*/
|
||||
|
||||
const ColorModeSchema = Joi.object({
|
||||
defaultMode: Joi.string().equal('dark', 'light').default('light'),
|
||||
disableSwitch: Joi.bool().default(false),
|
||||
respectPrefersColorScheme: Joi.bool().default(false),
|
||||
}).default({
|
||||
defaultMode: 'light',
|
||||
disableSwitch: false,
|
||||
respectPrefersColorScheme: false,
|
||||
});
|
||||
|
||||
const ThemeConfigSchema = Joi.object({
|
||||
// TODO temporary (@alpha-58)
|
||||
disableDarkMode: Joi.any().forbidden(false).messages({
|
||||
'any.unknown':
|
||||
'disableDarkMode theme config is deprecated. Please use the new colorMode attribute. You likely want: config.themeConfig.colorMode.disableSwitch = true',
|
||||
}),
|
||||
// TODO temporary (@alpha-58)
|
||||
defaultDarkMode: Joi.any().forbidden(false).messages({
|
||||
'any.unknown':
|
||||
'defaultDarkMode theme config is deprecated. Please use the new colorMode attribute. You likely want: config.themeConfig.colorMode.defaultMode = "dark"',
|
||||
}),
|
||||
colorMode: ColorModeSchema,
|
||||
image: Joi.string(),
|
||||
announcementBar: Joi.object({
|
||||
id: Joi.string(),
|
||||
content: Joi.string(),
|
||||
backgroundColor: Joi.string().default('#fff'),
|
||||
textColor: Joi.string().default('#000'),
|
||||
}).optional(),
|
||||
navbar: Joi.object({
|
||||
hideOnScroll: Joi.bool().default(false),
|
||||
// TODO temporary (@alpha-58)
|
||||
links: Joi.any().forbidden().messages({
|
||||
'any.unknown':
|
||||
'themeConfig.navbar.links has been renamed as themeConfig.navbar.items',
|
||||
}),
|
||||
items: Joi.array().items(NavbarItemSchema),
|
||||
title: Joi.string().required(),
|
||||
logo: Joi.object({
|
||||
alt: Joi.string(),
|
||||
src: Joi.string().required(),
|
||||
srcDark: Joi.string(),
|
||||
href: Joi.string(),
|
||||
target: Joi.string(),
|
||||
}),
|
||||
}),
|
||||
footer: Joi.object({
|
||||
style: Joi.string().equal('dark', 'light').default('light'),
|
||||
logo: Joi.object({
|
||||
alt: Joi.string(),
|
||||
src: Joi.string(),
|
||||
href: Joi.string(),
|
||||
}),
|
||||
copyright: Joi.string(),
|
||||
links: Joi.array().items(
|
||||
Joi.object({
|
||||
title: Joi.string().required(),
|
||||
items: Joi.array().items(
|
||||
Joi.object({
|
||||
to: Joi.string(),
|
||||
href: Joi.string().uri(),
|
||||
html: Joi.string(),
|
||||
label: Joi.string(),
|
||||
})
|
||||
.xor('to', 'href', 'html')
|
||||
.with('to', 'label')
|
||||
.with('href', 'label')
|
||||
.nand('html', 'label'),
|
||||
),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
module.exports = ThemeConfigSchema;
|
12
packages/docusaurus-types/src/index.d.ts
vendored
12
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -65,6 +65,7 @@ export interface DocusaurusSiteMetadata {
|
|||
export interface DocusaurusContext {
|
||||
siteConfig: DocusaurusConfig;
|
||||
siteMetadata: DocusaurusSiteMetadata;
|
||||
globalData: Record<string, any>;
|
||||
isClient: boolean;
|
||||
}
|
||||
|
||||
|
@ -117,9 +118,11 @@ export interface Props extends LoadContext, InjectedHtmlTags {
|
|||
export interface PluginContentLoadedActions {
|
||||
addRoute(config: RouteConfig): void;
|
||||
createData(name: string, data: any): Promise<string>;
|
||||
setGlobalData<T = unknown>(data: T): void;
|
||||
}
|
||||
|
||||
export interface Plugin<T, U = unknown> {
|
||||
id?: string;
|
||||
name: string;
|
||||
loadContent?(): Promise<T>;
|
||||
validateOptions?(): ValidationResult<U>;
|
||||
|
@ -154,10 +157,9 @@ export interface Plugin<T, U = unknown> {
|
|||
export type ConfigureWebpackFn = Plugin<unknown>['configureWebpack'];
|
||||
export type ConfigureWebpackFnMergeStrategy = Record<string, MergeStrategy>;
|
||||
|
||||
export type PluginConfig =
|
||||
| [string, Record<string, unknown>]
|
||||
| [string]
|
||||
| string;
|
||||
export type PluginOptions = {id?: string} & Record<string, unknown>;
|
||||
|
||||
export type PluginConfig = [string, PluginOptions] | [string] | string;
|
||||
|
||||
export interface ChunkRegistry {
|
||||
loader: string;
|
||||
|
@ -248,7 +250,9 @@ export interface ThemeConfigValidationContext<T, E extends Error = Error> {
|
|||
themeConfig: Partial<T>;
|
||||
}
|
||||
|
||||
// TODO we should use a Joi type here
|
||||
export interface ValidationSchema<T> {
|
||||
validate(options: Partial<T>, opt: object): ValidationResult<T>;
|
||||
unknown(): ValidationSchema<T>;
|
||||
append(data: any): ValidationSchema<T>;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import React, {useEffect, useState} from 'react';
|
|||
|
||||
import routes from '@generated/routes';
|
||||
import siteConfig from '@generated/docusaurus.config';
|
||||
import globalData from '@generated/globalData';
|
||||
import siteMetadata from '@generated/site-metadata';
|
||||
import renderRoutes from './exports/renderRoutes';
|
||||
import DocusaurusContext from './exports/context';
|
||||
|
@ -24,7 +25,8 @@ function App(): JSX.Element {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<DocusaurusContext.Provider value={{siteConfig, siteMetadata, isClient}}>
|
||||
<DocusaurusContext.Provider
|
||||
value={{siteConfig, siteMetadata, globalData, isClient}}>
|
||||
<PendingNavigation routes={routes}>
|
||||
{renderRoutes(routes)}
|
||||
</PendingNavigation>
|
||||
|
|
|
@ -20,11 +20,12 @@ declare global {
|
|||
interface Props {
|
||||
readonly isNavLink?: boolean;
|
||||
readonly to?: string;
|
||||
readonly activeClassName?: string;
|
||||
readonly href: string;
|
||||
readonly children?: ReactNode;
|
||||
}
|
||||
|
||||
function Link({isNavLink, ...props}: Props): JSX.Element {
|
||||
function Link({isNavLink, activeClassName, ...props}: Props): JSX.Element {
|
||||
const {to, href} = props;
|
||||
const targetLink = to || href;
|
||||
const isInternal = isInternalUrl(targetLink);
|
||||
|
@ -97,6 +98,8 @@ function Link({isNavLink, ...props}: Props): JSX.Element {
|
|||
onMouseEnter={onMouseEnter}
|
||||
innerRef={handleRef}
|
||||
to={targetLink}
|
||||
// avoid "React does not recognize the `activeClassName` prop on a DOM element"
|
||||
{...(isNavLink && {activeClassName})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
43
packages/docusaurus/src/client/exports/useGlobalData.ts
Normal file
43
packages/docusaurus/src/client/exports/useGlobalData.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* 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 useDocusaurusContext from './useDocusaurusContext';
|
||||
|
||||
export default function useGlobalData() {
|
||||
const {globalData} = useDocusaurusContext();
|
||||
if (!globalData) {
|
||||
throw new Error('Docusaurus global data not found');
|
||||
}
|
||||
return globalData;
|
||||
}
|
||||
|
||||
export function useAllPluginInstancesData<T = unknown>(
|
||||
pluginName: string,
|
||||
): Record<string, T> {
|
||||
const globalData = useGlobalData();
|
||||
const pluginGlobalData = globalData[pluginName];
|
||||
if (!pluginGlobalData) {
|
||||
throw new Error(
|
||||
`Docusaurus plugin global data not found for pluginName=${pluginName}`,
|
||||
);
|
||||
}
|
||||
return pluginGlobalData;
|
||||
}
|
||||
|
||||
export function usePluginData<T = unknown>(
|
||||
pluginName: string,
|
||||
pluginId: string = 'default',
|
||||
): T {
|
||||
const pluginGlobalData = useAllPluginInstancesData(pluginName);
|
||||
const pluginInstanceGlobalData = pluginGlobalData[pluginId];
|
||||
if (!pluginInstanceGlobalData) {
|
||||
throw new Error(
|
||||
`Docusaurus plugin global data not found for pluginName=${pluginName} and pluginId=${pluginId}`,
|
||||
);
|
||||
}
|
||||
return pluginInstanceGlobalData as T;
|
||||
}
|
|
@ -30,6 +30,7 @@ Object {
|
|||
],
|
||||
"@docusaurus/plugin-content-pages",
|
||||
],
|
||||
"presets": Array [],
|
||||
"projectName": "hello",
|
||||
"tagline": "Hello World",
|
||||
"themeConfig": Object {},
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ensureUniquePluginInstanceIds reject multi instance plugins with same id 1`] = `
|
||||
"Plugin plugin-docs is used 2 times with id=sameId.
|
||||
To use the same plugin multiple times on a Docusaurus site, you need to assign a unique id to each plugin instance."
|
||||
`;
|
||||
|
||||
exports[`ensureUniquePluginInstanceIds reject multi instance plugins without id 1`] = `
|
||||
"Plugin plugin-docs is used 2 times with id=default.
|
||||
To use the same plugin multiple times on a Docusaurus site, you need to assign a unique id to each plugin instance."
|
||||
`;
|
||||
|
||||
exports[`ensureUniquePluginInstanceIds reject multi instance plugins without id 2`] = `
|
||||
"Plugin plugin-docs is used 2 times with id=default.
|
||||
To use the same plugin multiple times on a Docusaurus site, you need to assign a unique id to each plugin instance."
|
||||
`;
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* 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 {ensureUniquePluginInstanceIds} from '../../plugins/pluginIds';
|
||||
|
||||
import {InitPlugin} from '../../plugins/init';
|
||||
|
||||
function createTestPlugin(name: string, id?: string): InitPlugin {
|
||||
// @ts-expect-error: good enough for tests
|
||||
return {
|
||||
name,
|
||||
options: {id},
|
||||
};
|
||||
}
|
||||
|
||||
describe('ensureUniquePluginInstanceIds', () => {
|
||||
test('accept single instance plugins', async () => {
|
||||
ensureUniquePluginInstanceIds([
|
||||
createTestPlugin('plugin-docs'),
|
||||
createTestPlugin('plugin-blog'),
|
||||
createTestPlugin('plugin-pages'),
|
||||
]);
|
||||
});
|
||||
|
||||
test('accept single instance plugins, all with sameId', async () => {
|
||||
ensureUniquePluginInstanceIds([
|
||||
createTestPlugin('plugin-docs', 'sameId'),
|
||||
createTestPlugin('plugin-blog', 'sameId'),
|
||||
createTestPlugin('plugin-pages', 'sameId'),
|
||||
]);
|
||||
});
|
||||
|
||||
test('accept multi instance plugins without id', async () => {
|
||||
ensureUniquePluginInstanceIds([
|
||||
createTestPlugin('plugin-docs', 'ios'),
|
||||
createTestPlugin('plugin-docs', 'android'),
|
||||
createTestPlugin('plugin-pages', 'pages'),
|
||||
]);
|
||||
});
|
||||
|
||||
test('reject multi instance plugins without id', async () => {
|
||||
expect(() =>
|
||||
ensureUniquePluginInstanceIds([
|
||||
createTestPlugin('plugin-docs'),
|
||||
createTestPlugin('plugin-docs'),
|
||||
]),
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test('reject multi instance plugins with same id', async () => {
|
||||
expect(() =>
|
||||
ensureUniquePluginInstanceIds([
|
||||
createTestPlugin('plugin-docs', 'sameId'),
|
||||
createTestPlugin('plugin-docs', 'sameId'),
|
||||
]),
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test('reject multi instance plugins without id', async () => {
|
||||
expect(() =>
|
||||
ensureUniquePluginInstanceIds([
|
||||
createTestPlugin('plugin-docs'),
|
||||
createTestPlugin('plugin-docs', 'ios'),
|
||||
createTestPlugin('plugin-docs'),
|
||||
createTestPlugin('plugin-pages'),
|
||||
createTestPlugin('plugin-pages', 'pages2'),
|
||||
]),
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
});
|
|
@ -12,6 +12,7 @@ import {CONFIG_FILE_NAME} from '../constants';
|
|||
export const DEFAULT_CONFIG: {
|
||||
plugins: PluginConfig[];
|
||||
themes: PluginConfig[];
|
||||
presets: PluginConfig[];
|
||||
customFields: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
@ -21,10 +22,26 @@ export const DEFAULT_CONFIG: {
|
|||
} = {
|
||||
plugins: [],
|
||||
themes: [],
|
||||
presets: [],
|
||||
customFields: {},
|
||||
themeConfig: {},
|
||||
};
|
||||
|
||||
const PluginSchema = Joi.alternatives().try(
|
||||
Joi.string(),
|
||||
Joi.array().items(Joi.string().required(), Joi.object().required()).length(2),
|
||||
);
|
||||
|
||||
const ThemeSchema = Joi.alternatives().try(
|
||||
Joi.string(),
|
||||
Joi.array().items(Joi.string().required(), Joi.object().required()).length(2),
|
||||
);
|
||||
|
||||
const PresetSchema = Joi.alternatives().try(
|
||||
Joi.string(),
|
||||
Joi.array().items(Joi.string().required(), Joi.object().required()).length(2),
|
||||
);
|
||||
|
||||
const ConfigSchema = Joi.object({
|
||||
baseUrl: Joi.string()
|
||||
.required()
|
||||
|
@ -37,33 +54,9 @@ const ConfigSchema = Joi.object({
|
|||
projectName: Joi.string(),
|
||||
customFields: Joi.object().unknown().default(DEFAULT_CONFIG.customFields),
|
||||
githubHost: Joi.string(),
|
||||
plugins: Joi.array()
|
||||
.items(
|
||||
Joi.alternatives().try(
|
||||
Joi.string(),
|
||||
Joi.array()
|
||||
.items(Joi.string().required(), Joi.object().required())
|
||||
.length(2),
|
||||
),
|
||||
)
|
||||
.default(DEFAULT_CONFIG.plugins),
|
||||
themes: Joi.array()
|
||||
.items(
|
||||
Joi.alternatives().try(
|
||||
Joi.string(),
|
||||
Joi.array()
|
||||
.items(Joi.string().required(), Joi.object().required())
|
||||
.length(2),
|
||||
),
|
||||
)
|
||||
.default(DEFAULT_CONFIG.themes),
|
||||
presets: Joi.array().items(
|
||||
Joi.alternatives().try(
|
||||
Joi.string(),
|
||||
Joi.array().items(Joi.string(), Joi.object()).length(2),
|
||||
),
|
||||
),
|
||||
|
||||
plugins: Joi.array().items(PluginSchema).default(DEFAULT_CONFIG.plugins),
|
||||
themes: Joi.array().items(ThemeSchema).default(DEFAULT_CONFIG.themes),
|
||||
presets: Joi.array().items(PresetSchema).default(DEFAULT_CONFIG.presets),
|
||||
themeConfig: Joi.object().unknown().default(DEFAULT_CONFIG.themeConfig),
|
||||
scripts: Joi.array().items(
|
||||
Joi.string(),
|
||||
|
|
|
@ -79,7 +79,7 @@ export async function load(
|
|||
|
||||
// Plugins.
|
||||
const pluginConfigs: PluginConfig[] = loadPluginConfigs(context);
|
||||
const {plugins, pluginsRouteConfigs} = await loadPlugins({
|
||||
const {plugins, pluginsRouteConfigs, globalData} = await loadPlugins({
|
||||
pluginConfigs,
|
||||
context,
|
||||
});
|
||||
|
@ -98,6 +98,7 @@ export async function load(
|
|||
const {stylesheets = [], scripts = []} = siteConfig;
|
||||
plugins.push({
|
||||
name: 'docusaurus-bootstrap-plugin',
|
||||
options: {},
|
||||
version: {type: 'synthetic'},
|
||||
configureWebpack: () => ({
|
||||
resolve: {
|
||||
|
@ -181,6 +182,12 @@ ${Object.keys(registry)
|
|||
|
||||
const genRoutes = generate(generatedFilesDir, 'routes.js', routesConfig);
|
||||
|
||||
const genGlobalData = generate(
|
||||
generatedFilesDir,
|
||||
'globalData.json',
|
||||
JSON.stringify(globalData, null, 2),
|
||||
);
|
||||
|
||||
// Version metadata.
|
||||
const siteMetadata: DocusaurusSiteMetadata = {
|
||||
docusaurusVersion: getPackageJsonVersion(
|
||||
|
@ -206,6 +213,7 @@ ${Object.keys(registry)
|
|||
genRegistry,
|
||||
genRoutesChunkNames,
|
||||
genRoutes,
|
||||
genGlobalData,
|
||||
genSiteMetadata,
|
||||
]);
|
||||
|
||||
|
|
|
@ -14,7 +14,9 @@ import {
|
|||
PluginContentLoadedActions,
|
||||
RouteConfig,
|
||||
} from '@docusaurus/types';
|
||||
import initPlugins, {PluginWithVersionInformation} from './init';
|
||||
import initPlugins, {InitPlugin} from './init';
|
||||
|
||||
const DefaultPluginId = 'default';
|
||||
|
||||
export function sortConfig(routeConfigs: RouteConfig[]): void {
|
||||
// Sort the route config. This ensures that route with nested
|
||||
|
@ -52,11 +54,12 @@ export async function loadPlugins({
|
|||
pluginConfigs: PluginConfig[];
|
||||
context: LoadContext;
|
||||
}): Promise<{
|
||||
plugins: PluginWithVersionInformation[];
|
||||
plugins: InitPlugin[];
|
||||
pluginsRouteConfigs: RouteConfig[];
|
||||
globalData: any;
|
||||
}> {
|
||||
// 1. Plugin Lifecycle - Initialization/Constructor.
|
||||
const plugins: PluginWithVersionInformation[] = initPlugins({
|
||||
const plugins: InitPlugin[] = initPlugins({
|
||||
pluginConfigs,
|
||||
context,
|
||||
});
|
||||
|
@ -78,25 +81,50 @@ export async function loadPlugins({
|
|||
// 3. Plugin Lifecycle - contentLoaded.
|
||||
const pluginsRouteConfigs: RouteConfig[] = [];
|
||||
|
||||
const globalData = {};
|
||||
|
||||
await Promise.all(
|
||||
plugins.map(async (plugin, index) => {
|
||||
if (!plugin.contentLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pluginId = plugin.options.id ?? DefaultPluginId;
|
||||
|
||||
const pluginContentDir = path.join(
|
||||
context.generatedFilesDir,
|
||||
plugin.name,
|
||||
// TODO each plugin instance should have its folder
|
||||
// pluginId,
|
||||
);
|
||||
|
||||
const addRoute: PluginContentLoadedActions['addRoute'] = (config) =>
|
||||
pluginsRouteConfigs.push(config);
|
||||
|
||||
const createData: PluginContentLoadedActions['createData'] = async (
|
||||
name,
|
||||
content,
|
||||
) => {
|
||||
const modulePath = path.join(pluginContentDir, name);
|
||||
await fs.ensureDir(path.dirname(modulePath));
|
||||
await generate(pluginContentDir, name, content);
|
||||
return modulePath;
|
||||
};
|
||||
|
||||
// the plugins global data are namespaced to avoid data conflicts:
|
||||
// - by plugin name
|
||||
// - by plugin id (allow using multiple instances of the same plugin)
|
||||
const setGlobalData: PluginContentLoadedActions['setGlobalData'] = (
|
||||
data,
|
||||
) => {
|
||||
globalData[plugin.name] = globalData[plugin.name] ?? {};
|
||||
globalData[plugin.name][pluginId] = data;
|
||||
};
|
||||
|
||||
const actions: PluginContentLoadedActions = {
|
||||
addRoute: (config) => pluginsRouteConfigs.push(config),
|
||||
createData: async (name, content) => {
|
||||
const modulePath = path.join(pluginContentDir, name);
|
||||
await fs.ensureDir(path.dirname(modulePath));
|
||||
await generate(pluginContentDir, name, content);
|
||||
return modulePath;
|
||||
},
|
||||
addRoute,
|
||||
createData,
|
||||
setGlobalData,
|
||||
};
|
||||
|
||||
await plugin.contentLoaded({
|
||||
|
@ -127,5 +155,6 @@ export async function loadPlugins({
|
|||
return {
|
||||
plugins,
|
||||
pluginsRouteConfigs,
|
||||
globalData,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -11,15 +11,26 @@ import importFresh from 'import-fresh';
|
|||
import {
|
||||
LoadContext,
|
||||
Plugin,
|
||||
PluginOptions,
|
||||
PluginConfig,
|
||||
ValidationSchema,
|
||||
DocusaurusPluginVersionInformation,
|
||||
} from '@docusaurus/types';
|
||||
import {CONFIG_FILE_NAME} from '../../constants';
|
||||
import {getPluginVersion} from '../versions';
|
||||
import {ensureUniquePluginInstanceIds} from './pluginIds';
|
||||
import * as Joi from '@hapi/joi';
|
||||
|
||||
function validate<T>(schema: ValidationSchema<T>, options: Partial<T>) {
|
||||
const {error, value} = schema.validate(options, {
|
||||
function pluginOptionsValidator<T>(
|
||||
schema: ValidationSchema<T>,
|
||||
options: Partial<T>,
|
||||
) {
|
||||
// All plugins can be provided an "id" for multi-instance support
|
||||
// we don't ask the user to implement id validation, we add it automatically
|
||||
const finalSchema = schema.append({
|
||||
id: Joi.string(),
|
||||
});
|
||||
const {error, value} = finalSchema.validate(options, {
|
||||
convert: false,
|
||||
});
|
||||
if (error) {
|
||||
|
@ -28,8 +39,15 @@ function validate<T>(schema: ValidationSchema<T>, options: Partial<T>) {
|
|||
return value;
|
||||
}
|
||||
|
||||
function validateAndStrip<T>(schema: ValidationSchema<T>, options: Partial<T>) {
|
||||
const {error, value} = schema.unknown().validate(options, {
|
||||
function themeConfigValidator<T>(
|
||||
schema: ValidationSchema<T>,
|
||||
options: Partial<T>,
|
||||
) {
|
||||
// A theme should only validate his "slice" of the full themeConfig,
|
||||
// not the whole object, so we allow unknown attributes to pass a theme validation
|
||||
const finalSchema = schema.unknown();
|
||||
|
||||
const {error, value} = finalSchema.validate(options, {
|
||||
convert: false,
|
||||
});
|
||||
|
||||
|
@ -39,7 +57,8 @@ function validateAndStrip<T>(schema: ValidationSchema<T>, options: Partial<T>) {
|
|||
return value;
|
||||
}
|
||||
|
||||
export type PluginWithVersionInformation = Plugin<unknown> & {
|
||||
export type InitPlugin = Plugin<unknown> & {
|
||||
readonly options: PluginOptions;
|
||||
readonly version: DocusaurusPluginVersionInformation;
|
||||
};
|
||||
|
||||
|
@ -49,7 +68,7 @@ export default function initPlugins({
|
|||
}: {
|
||||
pluginConfigs: PluginConfig[];
|
||||
context: LoadContext;
|
||||
}): PluginWithVersionInformation[] {
|
||||
}): InitPlugin[] {
|
||||
// We need to resolve plugins from the perspective of the siteDir, since the siteDir's package.json
|
||||
// declares the dependency on these plugins.
|
||||
// We need to fallback to createRequireFromPath since createRequire is only available in node v12.
|
||||
|
@ -57,10 +76,10 @@ export default function initPlugins({
|
|||
const createRequire = Module.createRequire || Module.createRequireFromPath;
|
||||
const pluginRequire = createRequire(join(context.siteDir, CONFIG_FILE_NAME));
|
||||
|
||||
const plugins: PluginWithVersionInformation[] = pluginConfigs
|
||||
const plugins: InitPlugin[] = pluginConfigs
|
||||
.map((pluginItem) => {
|
||||
let pluginModuleImport: string | undefined;
|
||||
let pluginOptions = {};
|
||||
let pluginOptions: PluginOptions = {};
|
||||
|
||||
if (!pluginItem) {
|
||||
return null;
|
||||
|
@ -90,7 +109,7 @@ export default function initPlugins({
|
|||
|
||||
if (validateOptions) {
|
||||
const normalizedOptions = validateOptions({
|
||||
validate,
|
||||
validate: pluginOptionsValidator,
|
||||
options: pluginOptions,
|
||||
});
|
||||
pluginOptions = normalizedOptions;
|
||||
|
@ -103,7 +122,7 @@ export default function initPlugins({
|
|||
|
||||
if (validateThemeConfig) {
|
||||
const normalizedThemeConfig = validateThemeConfig({
|
||||
validate: validateAndStrip,
|
||||
validate: themeConfigValidator,
|
||||
themeConfig: context.siteConfig.themeConfig,
|
||||
});
|
||||
|
||||
|
@ -112,8 +131,16 @@ export default function initPlugins({
|
|||
...normalizedThemeConfig,
|
||||
};
|
||||
}
|
||||
return {...plugin(context, pluginOptions), version: pluginVersion};
|
||||
|
||||
return {
|
||||
...plugin(context, pluginOptions),
|
||||
options: pluginOptions,
|
||||
version: pluginVersion,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
ensureUniquePluginInstanceIds(plugins);
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
|
30
packages/docusaurus/src/server/plugins/pluginIds.ts
Normal file
30
packages/docusaurus/src/server/plugins/pluginIds.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* 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 {groupBy} from 'lodash';
|
||||
import {InitPlugin} from './init';
|
||||
|
||||
// It is forbidden to have 2 plugins of the same name sharind the same id
|
||||
// this is required to support multi-instance plugins without conflict
|
||||
export function ensureUniquePluginInstanceIds(plugins: InitPlugin[]) {
|
||||
const pluginsByName = groupBy(plugins, (p) => p.name);
|
||||
Object.entries(pluginsByName).forEach(([pluginName, pluginInstances]) => {
|
||||
const pluginInstancesById = groupBy(
|
||||
pluginInstances,
|
||||
(p) => p.options.id ?? 'default',
|
||||
);
|
||||
Object.entries(pluginInstancesById).forEach(
|
||||
([pluginId, pluginInstancesWithId]) => {
|
||||
if (pluginInstancesWithId.length !== 1) {
|
||||
throw new Error(
|
||||
`Plugin ${pluginName} is used ${pluginInstancesWithId.length} times with id=${pluginId}.\nTo use the same plugin multiple times on a Docusaurus site, you need to assign a unique id to each plugin instance.`,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
|
@ -7,14 +7,14 @@ title: Blog
|
|||
|
||||
To setup your site's blog, start by creating a `blog` directory.
|
||||
|
||||
Then, add a navbar link to your blog within `docusaurus.config.js`:
|
||||
Then, add a item link to your blog within `docusaurus.config.js`:
|
||||
|
||||
```js title="docusaurus.config.js"
|
||||
module.exports = {
|
||||
themeConfig: {
|
||||
// ...
|
||||
navbar: {
|
||||
links: [
|
||||
items: [
|
||||
// ...
|
||||
// highlight-next-line
|
||||
{to: 'blog', label: 'Blog', position: 'left'}, // or position: 'right'
|
||||
|
|
|
@ -220,6 +220,84 @@ function Component() {
|
|||
}
|
||||
```
|
||||
|
||||
### `useGlobalData()`
|
||||
|
||||
React hook to access Docusaurus global data created by all the plugins.
|
||||
|
||||
Global data is namespaced by plugin name, and plugin id.
|
||||
|
||||
:::info
|
||||
|
||||
Plugin id is only useful when a plugin is used multiple times on the same site. Each plugin instance is able to create its own global data.
|
||||
|
||||
:::
|
||||
|
||||
```ts
|
||||
type GlobalData = Record<
|
||||
PluginName,
|
||||
Record<
|
||||
PluginId, // "default" by default
|
||||
any // plugin-specific data
|
||||
>
|
||||
>;
|
||||
```
|
||||
|
||||
Usage example:
|
||||
|
||||
```jsx {2,5,6,7}
|
||||
import React from 'react';
|
||||
import useGlobalData from '@docusaurus/useGlobalData';
|
||||
|
||||
const MyComponent = () => {
|
||||
const globalData = useDocusaurusContext();
|
||||
const myPluginData = globalData['my-plugin']['default'];
|
||||
return <div>{myPluginData.someAttribute}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
:::tip
|
||||
|
||||
Inspect your site's global data at `./docusaurus/globalData.json`
|
||||
|
||||
:::
|
||||
|
||||
### `usePluginData(pluginName: string, pluginId?: string)`
|
||||
|
||||
Access global data created by a specific plugin instance.
|
||||
|
||||
This is the most convenient hook to access plugin global data, and should be used most of the time.
|
||||
|
||||
`pluginId` is optional if you don't use multi-instance plugins.
|
||||
|
||||
Usage example:
|
||||
|
||||
```jsx {2,5,6}
|
||||
import React from 'react';
|
||||
import {usePluginData} from '@docusaurus/useGlobalData';
|
||||
|
||||
const MyComponent = () => {
|
||||
const myPluginData = usePluginData('my-plugin');
|
||||
return <div>{myPluginData.someAttribute}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
### `useAllPluginInstancesData(pluginName: string)`
|
||||
|
||||
Access global data created by a specific plugin. Given a plugin name, it returns the data of all the plugins instances of that name, by pluginId.
|
||||
|
||||
Usage example:
|
||||
|
||||
```jsx {2,5,6,7}
|
||||
import React from 'react';
|
||||
import {useAllPluginInstancesData} from '@docusaurus/useGlobalData';
|
||||
|
||||
const MyComponent = () => {
|
||||
const allPluginInstancesData = useAllPluginInstancesData('my-plugin');
|
||||
const myPluginData = allPluginInstancesData['default'];
|
||||
return <div>{myPluginData.someAttribute}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
### `ExecutionEnvironment`
|
||||
|
|
|
@ -150,7 +150,7 @@ module.exports = {
|
|||
alt: 'Site Logo',
|
||||
src: 'img/logo.svg',
|
||||
},
|
||||
links: [
|
||||
items: [
|
||||
{
|
||||
to: 'docs/docusaurus.config.js',
|
||||
activeBasePath: 'docs',
|
||||
|
|
|
@ -152,10 +152,6 @@ module.exports = function (context, options) {
|
|||
|
||||
Plugins should use the data loaded in `loadContent` and construct the pages/routes that consume the loaded data (optional).
|
||||
|
||||
## `async routesLoaded(routes)`
|
||||
|
||||
Plugins can modify the routes that were generated by all plugins. `routesLoaded` is called after `contentLoaded` hook.
|
||||
|
||||
### `content`
|
||||
|
||||
`contentLoaded` will be called _after_ `loadContent` is done, the return value of `loadContent()` will be passed to `contentLoaded` as `content`.
|
||||
|
@ -191,45 +187,98 @@ type Module =
|
|||
|
||||
- `createData(name: string, data: any): Promise<string>`
|
||||
|
||||
A helper function to help you write some data (usually a string or JSON) to disk with in-built caching. It takes a file name relative to to your plugin's directory **(name)**, your data **(data)**, and will return a path to where the data is created.
|
||||
A function to help you create static data (generally json or string), that you can provide to your routes as props.
|
||||
|
||||
For example, this plugin below create a `/roll` page which display "You won xxxx" to user.
|
||||
For example, this plugin below create a `/friends` page which display `Your friends are: Yangshun, Sebastien`:
|
||||
|
||||
```jsx title="website/src/components/roll.js"
|
||||
```jsx title="website/src/components/Friends.js"
|
||||
import React from 'react';
|
||||
|
||||
export default function (props) {
|
||||
const {prizes} = props;
|
||||
const index = Math.floor(Math.random() * 3);
|
||||
return <div> You won ${prizes[index]} </div>;
|
||||
export default function FriendsComponent({friends}) {
|
||||
return <div>Your friends are {friends.join(',')}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
```javascript {4-19} title="docusaurus-plugin/src/index.js"
|
||||
module.exports = function(context, options) {
|
||||
```js {4-23} title="docusaurus-friends-plugin/src/index.js"
|
||||
export default function friendsPlugin(context, options) {
|
||||
return {
|
||||
name: 'docusaurus-plugin',
|
||||
name: 'docusaurus-friends-plugin',
|
||||
async contentLoaded({content, actions}) {
|
||||
const {createData, addRoute} = actions;
|
||||
// Create a data named 'prizes.json'.
|
||||
const prizes = JSON.stringify(['$1', 'a cybertruck', 'nothing']);
|
||||
const prizesDataPath = await createData('prizes.json', prizes);
|
||||
// Create friends.json
|
||||
const friends = ['Yangshun', 'Sebastien'];
|
||||
const friendsJsonPath = await createData(
|
||||
'friends.json',
|
||||
JSON.stringify(friends),
|
||||
);
|
||||
|
||||
// Add '/roll' page using 'website/src/component/roll.js` as the component
|
||||
// and providing 'prizes' as props.
|
||||
// Add the '/friends' routes, and ensure it receives the friends props
|
||||
addRoute({
|
||||
path: '/roll',
|
||||
component: '@site/src/components/roll.js',
|
||||
path: '/friends',
|
||||
component: '@site/src/components/Friends.js',
|
||||
modules: {
|
||||
prizes: prizesDataPath
|
||||
}
|
||||
// propName -> json file path
|
||||
friends: friendsJsonPath,
|
||||
},
|
||||
exact: true,
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
- `setGlobalData(data: any): void`
|
||||
|
||||
This function permits to create some global plugin data, that can be read from any page, including the pages created by other plugins, and your theme layout.
|
||||
|
||||
This data become accessible to your client-side/theme code, through the [`useGlobalData`](./docusaurus-core.md#useglobaldata) and [`usePluginData`](./docusaurus-core.md#useplugindatapluginname-string-pluginid-string)
|
||||
|
||||
One this data is created, you can access it with the global data hooks APIs
|
||||
|
||||
:::caution
|
||||
|
||||
Global data is... global: its size affects the loading time of all pages of your site, so try to keep it small.
|
||||
|
||||
Prefer `createData` and page-specific data whenever possible.
|
||||
|
||||
:::
|
||||
|
||||
For example, this plugin below create a `/friends` page which display `Your friends are: Yangshun, Sebastien`:
|
||||
|
||||
```jsx title="website/src/components/Friends.js"
|
||||
import React from 'react';
|
||||
import {usePluginData} from '@docusaurus/useGlobalData';
|
||||
|
||||
export default function FriendsComponent() {
|
||||
const {friends} = usePluginData('my-friends-plugin');
|
||||
return <div>Your friends are {friends.join(',')}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
```js {4-14} title="docusaurus-friends-plugin/src/index.js"
|
||||
export default function friendsPlugin(context, options) {
|
||||
return {
|
||||
name: 'docusaurus-friends-plugin',
|
||||
async contentLoaded({content, actions}) {
|
||||
const {setGlobalData, addRoute} = actions;
|
||||
// Create friends global data
|
||||
setGlobalData({friends: ['Yangshun', 'Sebastien']});
|
||||
|
||||
// Add the '/friends' routes
|
||||
addRoute({
|
||||
path: '/friends',
|
||||
component: '@site/src/components/Friends.js',
|
||||
exact: true,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## `async routesLoaded(routes)`
|
||||
|
||||
Plugins can modify the routes that were generated by all plugins. `routesLoaded` is called after `contentLoaded` hook.
|
||||
|
||||
## `configureWebpack(config, isServer, utils)`
|
||||
|
||||
Modifies the internal webpack config. If the return value is a JavaScript object, it will be merged into the final config using [`webpack-merge`](https://github.com/survivejs/webpack-merge). If it is a function, it will be called and receive `config` as the first argument and an `isServer` flag as the argument argument.
|
||||
|
|
|
@ -295,7 +295,7 @@ module.exports = {
|
|||
alt: 'Docusaurus Logo',
|
||||
src: 'img/docusaurus.svg',
|
||||
},
|
||||
links: [
|
||||
items: [
|
||||
{to: 'docs/doc1', label: 'Getting Started', position: 'left'},
|
||||
{to: 'help', label: 'Help', position: 'left'},
|
||||
{
|
||||
|
|
|
@ -148,16 +148,18 @@ module.exports = {
|
|||
};
|
||||
```
|
||||
|
||||
### Navbar links
|
||||
### Navbar items
|
||||
|
||||
You can add links to the navbar via `themeConfig.navbar.links`:
|
||||
You can add items to the navbar via `themeConfig.navbar.items`.
|
||||
|
||||
By default, Navbar items are regular links (internal or external).
|
||||
|
||||
```js {5-15} title="docusaurus.config.js"
|
||||
module.exports = {
|
||||
// ...
|
||||
themeConfig: {
|
||||
navbar: {
|
||||
links: [
|
||||
items: [
|
||||
{
|
||||
// Client-side routing, used for navigating within the website.
|
||||
// The baseUrl will be automatically prepended to this value.
|
||||
|
@ -180,7 +182,7 @@ module.exports = {
|
|||
// Custom CSS class (for styling any item).
|
||||
className: '',
|
||||
},
|
||||
// ... other links
|
||||
// ... other items
|
||||
],
|
||||
},
|
||||
// ...
|
||||
|
@ -194,14 +196,14 @@ Outbound (external) links automatically get `target="_blank" rel="noopener noref
|
|||
|
||||
### Navbar dropdown
|
||||
|
||||
Navbar items can also be dropdown items by specifying the `items`, an inner array of navbar links.
|
||||
Navbar items can also be dropdown items by specifying the `items`, an inner array of navbar items.
|
||||
|
||||
```js {9-19} title="docusaurus.config.js"
|
||||
module.exports = {
|
||||
// ...
|
||||
themeConfig: {
|
||||
navbar: {
|
||||
links: [
|
||||
items: [
|
||||
{
|
||||
label: 'Community',
|
||||
position: 'left', // or 'right'
|
||||
|
@ -224,6 +226,46 @@ module.exports = {
|
|||
};
|
||||
```
|
||||
|
||||
### Navbar docs version dropdown
|
||||
|
||||
If you use docs with versioning, this special navbar item type that will render a dropdown with all your site's available versions. The user will be able to switch from one version to another, while staying on the same doc (as long as the doc id is constant across versions).
|
||||
|
||||
```js {5-8} title="docusaurus.config.js"
|
||||
module.exports = {
|
||||
themeConfig: {
|
||||
navbar: {
|
||||
items: [
|
||||
{
|
||||
type: 'docsVersionDropdown',
|
||||
position: 'left',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Navbar docs version
|
||||
|
||||
If you use docs with versioning, this special navbar item type will link to the active/browsed version of your doc (depends on the current url), and fallback to the latest version.
|
||||
|
||||
```js {5-10} title="docusaurus.config.js"
|
||||
module.exports = {
|
||||
themeConfig: {
|
||||
navbar: {
|
||||
items: [
|
||||
{
|
||||
type: 'docsVersion',
|
||||
position: 'left',
|
||||
// to: "/path // by default, link to active/latest version
|
||||
// label: "label" // by default, show active/latest version label
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Auto-hide sticky navbar
|
||||
|
||||
You can enable this cool UI feature that automatically hides the navbar when a user starts scrolling down the page, and show it again when the user scrolls up.
|
||||
|
|
|
@ -72,6 +72,35 @@ module.exports = {
|
|||
};
|
||||
```
|
||||
|
||||
## Multi-instance plugins and plugin ids
|
||||
|
||||
It is possible to use multiple times the same plugin, on the same Docusaurus website.
|
||||
|
||||
In this case, you it is required to assign a unique id to each plugin instance.
|
||||
|
||||
By default, the plugin id is `default`.
|
||||
|
||||
```js {6,13} title="docusaurus.config.js"
|
||||
module.exports = {
|
||||
plugins: [
|
||||
[
|
||||
'@docusaurus/plugin-xxx',
|
||||
{
|
||||
id: 'plugin-xxx-1',
|
||||
// other options
|
||||
},
|
||||
],
|
||||
[
|
||||
'@docusaurus/plugin-xxx',
|
||||
{
|
||||
id: 'plugin-xxx-2',
|
||||
// other options
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
## Plugins design
|
||||
|
||||
Docusaurus' implementation of the plugins system provides us with a convenient way to hook into the website's lifecycle to modify what goes on during development/build, which involves (but not limited to) extending the webpack config, modifying the data being loaded and creating new components to be used in a page.
|
||||
|
|
|
@ -173,28 +173,10 @@ module.exports = {
|
|||
src: 'img/docusaurus.svg',
|
||||
srcDark: 'img/docusaurus_keytar.svg',
|
||||
},
|
||||
links: [
|
||||
items: [
|
||||
{
|
||||
label: 'Docs',
|
||||
to: 'docs', // "fake" link
|
||||
type: 'docsVersionDropdown',
|
||||
position: 'left',
|
||||
activeBaseRegex: `docs/(?!next/(support|team|resources))`,
|
||||
items: [
|
||||
{
|
||||
label: versions[0],
|
||||
to: 'docs/',
|
||||
activeBaseRegex: `docs/(?!${versions.join('|')}|next)`,
|
||||
},
|
||||
...versions.slice(1).map((version) => ({
|
||||
label: version,
|
||||
to: `docs/${version}/`,
|
||||
})),
|
||||
{
|
||||
label: 'Master/Unreleased',
|
||||
to: 'docs/next/',
|
||||
activeBaseRegex: `docs/next/(?!support|team|resources)`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{to: 'blog', label: 'Blog', position: 'left'},
|
||||
{to: 'showcase', label: 'Showcase', position: 'left'},
|
||||
|
@ -205,8 +187,8 @@ module.exports = {
|
|||
activeBaseRegex: `docs/next/(support|team|resources)`,
|
||||
},
|
||||
{
|
||||
type: 'docsVersion',
|
||||
to: 'versions',
|
||||
label: `v${versions[0]}`,
|
||||
position: 'right',
|
||||
},
|
||||
{
|
||||
|
|
|
@ -3083,6 +3083,13 @@
|
|||
dependencies:
|
||||
"@types/lodash" "*"
|
||||
|
||||
"@types/lodash.sortby@^4.6.6":
|
||||
version "4.7.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash.sortby/-/lodash.sortby-4.7.6.tgz#eed689835f274b553db4ae16a4a23f58b79618a1"
|
||||
integrity sha512-EnvAOmKvEg7gdYpYrS6+fVFPw5dL9rBnJi3vcKI7wqWQcLJVF/KRXK9dH29HjGNVvFUj0s9prRP3J8jEGnGKDw==
|
||||
dependencies:
|
||||
"@types/lodash" "*"
|
||||
|
||||
"@types/lodash@*", "@types/lodash@^4.14.53":
|
||||
version "4.14.149"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440"
|
||||
|
@ -11646,7 +11653,7 @@ lodash.some@^4.4.0:
|
|||
resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d"
|
||||
integrity sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=
|
||||
|
||||
lodash.sortby@^4.7.0:
|
||||
lodash.sortby@^4.6.0, lodash.sortby@^4.7.0:
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
||||
|
|
Loading…
Add table
Reference in a new issue