diff --git a/.eslintrc.js b/.eslintrc.js index 8bd8f0869b..99de6c9c66 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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, diff --git a/jest.config.js b/jest.config.js index 8bb2ddeeff..69003e28ef 100644 --- a/jest.config.js +++ b/jest.config.js @@ -28,4 +28,7 @@ module.exports = { '^.+\\.[jt]sx?$': 'babel-jest', }, setupFiles: ['./jest/stylelint-rule-test.js'], + moduleNameMapper: { + '@docusaurus/router': 'react-router-dom', + }, }; diff --git a/package.json b/package.json index e07020cc5e..a17788e84c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/docusaurus-init/templates/bootstrap/docusaurus.config.js b/packages/docusaurus-init/templates/bootstrap/docusaurus.config.js index 27deb4d95d..c7704ecf87 100644 --- a/packages/docusaurus-init/templates/bootstrap/docusaurus.config.js +++ b/packages/docusaurus-init/templates/bootstrap/docusaurus.config.js @@ -13,7 +13,7 @@ module.exports = { alt: 'My Site Logo', src: 'img/logo.svg', }, - links: [ + items: [ { to: 'docs/', activeBasePath: 'docs', diff --git a/packages/docusaurus-init/templates/classic/docusaurus.config.js b/packages/docusaurus-init/templates/classic/docusaurus.config.js index 3ee5e1c0d8..aaed990098 100644 --- a/packages/docusaurus-init/templates/classic/docusaurus.config.js +++ b/packages/docusaurus-init/templates/classic/docusaurus.config.js @@ -13,7 +13,7 @@ module.exports = { alt: 'My Site Logo', src: 'img/logo.svg', }, - links: [ + items: [ { to: 'docs/', activeBasePath: 'docs', diff --git a/packages/docusaurus-init/templates/facebook/docusaurus.config.js b/packages/docusaurus-init/templates/facebook/docusaurus.config.js index d4c945c2d5..a5d7841394 100644 --- a/packages/docusaurus-init/templates/facebook/docusaurus.config.js +++ b/packages/docusaurus-init/templates/facebook/docusaurus.config.js @@ -22,7 +22,7 @@ module.exports = { alt: 'My Facebook Project Logo', src: 'img/logo.svg', }, - links: [ + items: [ { to: 'docs/', activeBasePath: 'docs', diff --git a/packages/docusaurus-module-type-aliases/src/index.d.ts b/packages/docusaurus-module-type-aliases/src/index.d.ts index 2862aa01a4..13176647d4 100644 --- a/packages/docusaurus-module-type-aliases/src/index.d.ts +++ b/packages/docusaurus-module-type-aliases/src/index.d.ts @@ -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/*'; diff --git a/packages/docusaurus-plugin-content-blog/src/index.ts b/packages/docusaurus-plugin-content-blog/src/index.ts index 6e9bfe98e6..bece97f97b 100644 --- a/packages/docusaurus-plugin-content-blog/src/index.ts +++ b/packages/docusaurus-plugin-content-blog/src/index.ts @@ -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[] = []; diff --git a/packages/docusaurus-plugin-content-blog/src/types.ts b/packages/docusaurus-plugin-content-blog/src/types.ts index 80e7a13cd8..3de088fb48 100644 --- a/packages/docusaurus-plugin-content-blog/src/types.ts +++ b/packages/docusaurus-plugin-content-blog/src/types.ts @@ -20,6 +20,7 @@ export interface DateLink { export type FeedType = 'rss' | 'atom'; export interface PluginOptions { + id?: string; path: string; routeBasePath: string; include: string[]; diff --git a/packages/docusaurus-plugin-content-docs/package.json b/packages/docusaurus-plugin-content-docs/package.json index 5416da59c9..ba503297ea 100644 --- a/packages/docusaurus-plugin-content-docs/package.json +++ b/packages/docusaurus-plugin-content-docs/package.json @@ -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" }, diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap index c96321e08f..02b55d0e02 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap @@ -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 [ diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/client/docsClientUtils.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/client/docsClientUtils.test.ts new file mode 100644 index 0000000000..7e300a068c --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/client/docsClientUtils.test.ts @@ -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 = { + 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, + }); + }); +}); diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts index 1fd4d6a2be..fcf209af70 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts @@ -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(); }); }); diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/metadata.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/metadata.test.ts index 029cb3ad46..6df72b6be6 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/metadata.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/metadata.test.ts @@ -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), diff --git a/packages/docusaurus-plugin-content-docs/src/client/docsClientUtils.ts b/packages/docusaurus-plugin-content-docs/src/client/docsClientUtils.ts new file mode 100644 index 0000000000..6db4a68f8a --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/client/docsClientUtils.ts @@ -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, + 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; +}; + +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}; +}; diff --git a/packages/docusaurus-plugin-content-docs/src/index.ts b/packages/docusaurus-plugin-content-docs/src/index.ts index 23467c66e6..e00ee58322 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/index.ts @@ -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(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 = 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) { diff --git a/packages/docusaurus-plugin-content-docs/src/metadata.ts b/packages/docusaurus-plugin-content-docs/src/metadata.ts index a27b9d0849..6b9ac2e7e8 100644 --- a/packages/docusaurus-plugin-content-docs/src/metadata.ts +++ b/packages/docusaurus-plugin-content-docs/src/metadata.ts @@ -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, diff --git a/packages/docusaurus-plugin-content-docs/src/theme/hooks/useDocs.ts b/packages/docusaurus-plugin-content-docs/src/theme/hooks/useDocs.ts new file mode 100644 index 0000000000..dccf871a0b --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/theme/hooks/useDocs.ts @@ -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 => + 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); +}; diff --git a/packages/docusaurus-plugin-content-docs/src/types.ts b/packages/docusaurus-plugin-content-docs/src/types.ts index 4825145a97..bb8374271d 100644 --- a/packages/docusaurus-plugin-content-docs/src/types.ts +++ b/packages/docusaurus-plugin-content-docs/src/types.ts @@ -5,6 +5,11 @@ * LICENSE file in the root directory of this source tree. */ +// eslint-disable-next-line spaced-comment +/// + +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[]; +}; diff --git a/packages/docusaurus-plugin-debug/package.json b/packages/docusaurus-plugin-debug/package.json index ecbfa43d62..0b4352f5bb 100644 --- a/packages/docusaurus-plugin-debug/package.json +++ b/packages/docusaurus-plugin-debug/package.json @@ -4,7 +4,8 @@ "description": "Debug plugin for Docusaurus", "main": "lib/index.js", "scripts": { - "build": "tsc" + "build": "tsc", + "watch": "tsc --watch" }, "publishConfig": { "access": "public" diff --git a/packages/docusaurus-theme-classic/src/index.js b/packages/docusaurus-theme-classic/src/index.js index 5a1c26ad4e..cff7286915 100644 --- a/packages/docusaurus-theme-classic/src/index.js +++ b/packages/docusaurus-theme-classic/src/index.js @@ -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); }; diff --git a/packages/docusaurus-theme-classic/src/theme/DocItem/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocItem/index.tsx index 93e996c9de..ba0652cb9a 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocItem/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocItem/index.tsx @@ -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 ( <> @@ -112,33 +110,7 @@ function DocItem(props): JSX.Element { className={clsx('col', { [styles.docItemCol]: !hideTableOfContents, })}> - {latestVersionMainDocPermalink && ( -
- {version === 'next' ? ( -
- This is unreleased documentation for {siteTitle}{' '} - {version} version. -
- ) : ( -
- This is archived documentation for {siteTitle}{' '} - v{version}, which is no longer actively - maintained. -
- )} -
- For up-to-date documentation, see the{' '} - - - latest version - - - . -
-
- )} +
{version && ( diff --git a/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx index 95c74c2b4b..2ee000fb4d 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx @@ -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 ; - } - + const sidebarName = permalinkToSidebar[currentDocRoute.path]; + const sidebar = docsSidebars[sidebarName]; return (
{sidebar && (
)}
- - {renderRoutes(baseRoute.routes)} - + {children}
); } +function DocPage(props) { + const { + route: {routes: docRoutes}, + docsMetadata, + location, + } = props; + const currentDocRoute = docRoutes.find((docRoute) => + matchPath(location.pathname, docRoute), + ); + if (!currentDocRoute) { + return ; + } + return ( + + {renderRoutes(docRoutes)} + + ); +} + export default DocPage; diff --git a/packages/docusaurus-theme-classic/src/theme/DocSidebar/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocSidebar/index.tsx index 5586586493..3ed8678b98 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocSidebar/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocSidebar/index.tsx @@ -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 (
    - {sidebarData.map((item) => ( + {sidebar.map((item) => ( { + 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 ( +
    + {activeVersionName === 'next' ? ( +
    + This is unreleased documentation for {siteTitle}{' '} + {activeVersionName} version. +
    + ) : ( +
    + This is documentation for {siteTitle}{' '} + v{activeVersionName}, which is no longer actively + maintained. +
    + )} +
    + For up-to-date documentation, see the{' '} + + latest version + {' '} + ({latestVersionSuggestion.name}). +
    +
    + ); +} + +export default DocVersionSuggestions; diff --git a/packages/docusaurus-theme-classic/src/theme/Layout/index.tsx b/packages/docusaurus-theme-classic/src/theme/Layout/index.tsx index b5ea1d72ba..d36de4c083 100644 --- a/packages/docusaurus-theme-classic/src/theme/Layout/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/Layout/index.tsx @@ -18,6 +18,14 @@ import Footer from '@theme/Footer'; import './styles.css'; +function Providers({children}) { + return ( + + {children} + + ); +} + type Props = { children: ReactNode; title?: string; @@ -53,42 +61,36 @@ function Layout(props: Props): JSX.Element { const faviconUrl = useBaseUrl(favicon); return ( - - - - {/* TODO: Do not assume that it is in english language */} - + + + {/* TODO: Do not assume that it is in english language */} + - {metaTitle && {metaTitle}} - {metaTitle && } - {favicon && } - {description && } - {description && ( - - )} - {version && } - {keywords && keywords.length && ( - - )} - {metaImage && } - {metaImage && ( - - )} - {metaImage && ( - - )} - {permalink && ( - - )} - {permalink && } - - - - -
    {children}
    - {!noFooter &&