mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-29 18:27:56 +02:00
refactor(v2): docs plugin refactor (#3245)
* safe refactorings * safe refactors * add code to read versions more generically * refactor docs plugin * refactors * stable docs refactor * progress on refactor * stable docs refactor * stable docs refactor * stable docs refactor * attempt to fix admonition :( * configureWebpack docs: better typing * more refactors * rename cli * refactor docs metadata processing => move to pure function * stable docs refactor * stable docs refactor * named exports * basic sidebars refactor * add getElementsAround utils * refactor sidebar + ordering/navigation logic * stable retrocompatible refactor * add proper versions metadata tests * fix docs metadata tests * fix docs tests * fix test due to absolute path * fix webpack tests * refactor linkify + add broken markdown links warning * fix DOM warning due to forwarding legacy prop to div element * add todo
This commit is contained in:
parent
d17df954b5
commit
a4c8a7f55b
54 changed files with 3219 additions and 2724 deletions
|
@ -87,6 +87,7 @@ module.exports = {
|
||||||
{functions: false, classes: false, variables: true},
|
{functions: false, classes: false, variables: true},
|
||||||
],
|
],
|
||||||
'no-unused-vars': OFF,
|
'no-unused-vars': OFF,
|
||||||
|
'no-nested-ternary': WARNING,
|
||||||
'@typescript-eslint/no-unused-vars': [ERROR, {argsIgnorePattern: '^_'}],
|
'@typescript-eslint/no-unused-vars': [ERROR, {argsIgnorePattern: '^_'}],
|
||||||
'@typescript-eslint/ban-ts-comment': [
|
'@typescript-eslint/ban-ts-comment': [
|
||||||
ERROR,
|
ERROR,
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@docusaurus/module-type-aliases": "^2.0.0-alpha.61",
|
"@docusaurus/module-type-aliases": "^2.0.0-alpha.61",
|
||||||
"@types/hapi__joi": "^17.1.2",
|
"@types/hapi__joi": "^17.1.2",
|
||||||
|
"@types/picomatch": "^2.2.1",
|
||||||
"commander": "^5.0.0",
|
"commander": "^5.0.0",
|
||||||
"picomatch": "^2.1.1"
|
"picomatch": "^2.1.1"
|
||||||
},
|
},
|
||||||
|
@ -37,7 +38,9 @@
|
||||||
"lodash.pickby": "^4.6.0",
|
"lodash.pickby": "^4.6.0",
|
||||||
"lodash.sortby": "^4.6.0",
|
"lodash.sortby": "^4.6.0",
|
||||||
"remark-admonitions": "^1.2.1",
|
"remark-admonitions": "^1.2.1",
|
||||||
"shelljs": "^0.8.4"
|
"shelljs": "^0.8.4",
|
||||||
|
"utility-types": "^3.10.0",
|
||||||
|
"webpack": "^4.41.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^16.8.4",
|
"react": "^16.8.4",
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"version-1.0.1/docs": {
|
||||||
|
"Test": ["version-withSlugs/rootAbsoluteSlug"]
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
|
@ -6,14 +6,14 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {docsVersion} from '../version';
|
import {cliDocsVersionCommand} from '../cli';
|
||||||
import {PathOptions} from '../types';
|
import {PathOptions} from '../types';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import {
|
import {
|
||||||
getVersionedDocsDir,
|
getVersionedDocsDirPath,
|
||||||
getVersionsJSONFile,
|
getVersionsFilePath,
|
||||||
getVersionedSidebarsDir,
|
getVersionedSidebarsDirPath,
|
||||||
} from '../env';
|
} from '../versions';
|
||||||
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
|
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
|
||||||
|
|
||||||
const fixtureDir = path.join(__dirname, '__fixtures__');
|
const fixtureDir = path.join(__dirname, '__fixtures__');
|
||||||
|
@ -28,17 +28,32 @@ describe('docsVersion', () => {
|
||||||
|
|
||||||
test('no version tag provided', () => {
|
test('no version tag provided', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
docsVersion(null, simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
|
cliDocsVersionCommand(
|
||||||
|
null,
|
||||||
|
simpleSiteDir,
|
||||||
|
DEFAULT_PLUGIN_ID,
|
||||||
|
DEFAULT_OPTIONS,
|
||||||
|
),
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"[docs] No version tag specified!. Pass the version you wish to create as an argument. Ex: 1.0.0"`,
|
`"[docs] No version tag specified!. Pass the version you wish to create as an argument. Ex: 1.0.0"`,
|
||||||
);
|
);
|
||||||
expect(() =>
|
expect(() =>
|
||||||
docsVersion(undefined, simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
|
cliDocsVersionCommand(
|
||||||
|
undefined,
|
||||||
|
simpleSiteDir,
|
||||||
|
DEFAULT_PLUGIN_ID,
|
||||||
|
DEFAULT_OPTIONS,
|
||||||
|
),
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"[docs] No version tag specified!. Pass the version you wish to create as an argument. Ex: 1.0.0"`,
|
`"[docs] No version tag specified!. Pass the version you wish to create as an argument. Ex: 1.0.0"`,
|
||||||
);
|
);
|
||||||
expect(() =>
|
expect(() =>
|
||||||
docsVersion('', simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
|
cliDocsVersionCommand(
|
||||||
|
'',
|
||||||
|
simpleSiteDir,
|
||||||
|
DEFAULT_PLUGIN_ID,
|
||||||
|
DEFAULT_OPTIONS,
|
||||||
|
),
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"[docs] No version tag specified!. Pass the version you wish to create as an argument. Ex: 1.0.0"`,
|
`"[docs] No version tag specified!. Pass the version you wish to create as an argument. Ex: 1.0.0"`,
|
||||||
);
|
);
|
||||||
|
@ -46,12 +61,17 @@ describe('docsVersion', () => {
|
||||||
|
|
||||||
test('version tag should not have slash', () => {
|
test('version tag should not have slash', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
docsVersion('foo/bar', simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
|
cliDocsVersionCommand(
|
||||||
|
'foo/bar',
|
||||||
|
simpleSiteDir,
|
||||||
|
DEFAULT_PLUGIN_ID,
|
||||||
|
DEFAULT_OPTIONS,
|
||||||
|
),
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"[docs] Invalid version tag specified! Do not include slash (/) or (\\\\). Try something like: 1.0.0"`,
|
`"[docs] Invalid version tag specified! Do not include slash (/) or (\\\\). Try something like: 1.0.0"`,
|
||||||
);
|
);
|
||||||
expect(() =>
|
expect(() =>
|
||||||
docsVersion(
|
cliDocsVersionCommand(
|
||||||
'foo\\bar',
|
'foo\\bar',
|
||||||
simpleSiteDir,
|
simpleSiteDir,
|
||||||
DEFAULT_PLUGIN_ID,
|
DEFAULT_PLUGIN_ID,
|
||||||
|
@ -64,7 +84,7 @@ describe('docsVersion', () => {
|
||||||
|
|
||||||
test('version tag should not be too long', () => {
|
test('version tag should not be too long', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
docsVersion(
|
cliDocsVersionCommand(
|
||||||
'a'.repeat(255),
|
'a'.repeat(255),
|
||||||
simpleSiteDir,
|
simpleSiteDir,
|
||||||
DEFAULT_PLUGIN_ID,
|
DEFAULT_PLUGIN_ID,
|
||||||
|
@ -77,12 +97,22 @@ describe('docsVersion', () => {
|
||||||
|
|
||||||
test('version tag should not be a dot or two dots', () => {
|
test('version tag should not be a dot or two dots', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
docsVersion('..', simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
|
cliDocsVersionCommand(
|
||||||
|
'..',
|
||||||
|
simpleSiteDir,
|
||||||
|
DEFAULT_PLUGIN_ID,
|
||||||
|
DEFAULT_OPTIONS,
|
||||||
|
),
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"[docs] Invalid version tag specified! Do not name your version \\".\\" or \\"..\\". Try something like: 1.0.0"`,
|
`"[docs] Invalid version tag specified! Do not name your version \\".\\" or \\"..\\". Try something like: 1.0.0"`,
|
||||||
);
|
);
|
||||||
expect(() =>
|
expect(() =>
|
||||||
docsVersion('.', simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
|
cliDocsVersionCommand(
|
||||||
|
'.',
|
||||||
|
simpleSiteDir,
|
||||||
|
DEFAULT_PLUGIN_ID,
|
||||||
|
DEFAULT_OPTIONS,
|
||||||
|
),
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"[docs] Invalid version tag specified! Do not name your version \\".\\" or \\"..\\". Try something like: 1.0.0"`,
|
`"[docs] Invalid version tag specified! Do not name your version \\".\\" or \\"..\\". Try something like: 1.0.0"`,
|
||||||
);
|
);
|
||||||
|
@ -90,7 +120,7 @@ describe('docsVersion', () => {
|
||||||
|
|
||||||
test('version tag should be a valid pathname', () => {
|
test('version tag should be a valid pathname', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
docsVersion(
|
cliDocsVersionCommand(
|
||||||
'<foo|bar>',
|
'<foo|bar>',
|
||||||
simpleSiteDir,
|
simpleSiteDir,
|
||||||
DEFAULT_PLUGIN_ID,
|
DEFAULT_PLUGIN_ID,
|
||||||
|
@ -100,7 +130,7 @@ describe('docsVersion', () => {
|
||||||
`"[docs] Invalid version tag specified! Please ensure its a valid pathname too. Try something like: 1.0.0"`,
|
`"[docs] Invalid version tag specified! Please ensure its a valid pathname too. Try something like: 1.0.0"`,
|
||||||
);
|
);
|
||||||
expect(() =>
|
expect(() =>
|
||||||
docsVersion(
|
cliDocsVersionCommand(
|
||||||
'foo\x00bar',
|
'foo\x00bar',
|
||||||
simpleSiteDir,
|
simpleSiteDir,
|
||||||
DEFAULT_PLUGIN_ID,
|
DEFAULT_PLUGIN_ID,
|
||||||
|
@ -110,7 +140,12 @@ describe('docsVersion', () => {
|
||||||
`"[docs] Invalid version tag specified! Please ensure its a valid pathname too. Try something like: 1.0.0"`,
|
`"[docs] Invalid version tag specified! Please ensure its a valid pathname too. Try something like: 1.0.0"`,
|
||||||
);
|
);
|
||||||
expect(() =>
|
expect(() =>
|
||||||
docsVersion('foo:bar', simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
|
cliDocsVersionCommand(
|
||||||
|
'foo:bar',
|
||||||
|
simpleSiteDir,
|
||||||
|
DEFAULT_PLUGIN_ID,
|
||||||
|
DEFAULT_OPTIONS,
|
||||||
|
),
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"[docs] Invalid version tag specified! Please ensure its a valid pathname too. Try something like: 1.0.0"`,
|
`"[docs] Invalid version tag specified! Please ensure its a valid pathname too. Try something like: 1.0.0"`,
|
||||||
);
|
);
|
||||||
|
@ -118,7 +153,7 @@ describe('docsVersion', () => {
|
||||||
|
|
||||||
test('version tag already exist', () => {
|
test('version tag already exist', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
docsVersion(
|
cliDocsVersionCommand(
|
||||||
'1.0.0',
|
'1.0.0',
|
||||||
versionedSiteDir,
|
versionedSiteDir,
|
||||||
DEFAULT_PLUGIN_ID,
|
DEFAULT_PLUGIN_ID,
|
||||||
|
@ -132,7 +167,12 @@ describe('docsVersion', () => {
|
||||||
test('no docs file to version', () => {
|
test('no docs file to version', () => {
|
||||||
const emptySiteDir = path.join(fixtureDir, 'empty-site');
|
const emptySiteDir = path.join(fixtureDir, 'empty-site');
|
||||||
expect(() =>
|
expect(() =>
|
||||||
docsVersion('1.0.0', emptySiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
|
cliDocsVersionCommand(
|
||||||
|
'1.0.0',
|
||||||
|
emptySiteDir,
|
||||||
|
DEFAULT_PLUGIN_ID,
|
||||||
|
DEFAULT_OPTIONS,
|
||||||
|
),
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"[docs] There is no docs to version !"`,
|
`"[docs] There is no docs to version !"`,
|
||||||
);
|
);
|
||||||
|
@ -159,23 +199,23 @@ describe('docsVersion', () => {
|
||||||
path: 'docs',
|
path: 'docs',
|
||||||
sidebarPath: path.join(simpleSiteDir, 'sidebars.json'),
|
sidebarPath: path.join(simpleSiteDir, 'sidebars.json'),
|
||||||
};
|
};
|
||||||
docsVersion('1.0.0', simpleSiteDir, DEFAULT_PLUGIN_ID, options);
|
cliDocsVersionCommand('1.0.0', simpleSiteDir, DEFAULT_PLUGIN_ID, options);
|
||||||
expect(copyMock).toHaveBeenCalledWith(
|
expect(copyMock).toHaveBeenCalledWith(
|
||||||
path.join(simpleSiteDir, options.path),
|
path.join(simpleSiteDir, options.path),
|
||||||
path.join(
|
path.join(
|
||||||
getVersionedDocsDir(simpleSiteDir, DEFAULT_PLUGIN_ID),
|
getVersionedDocsDirPath(simpleSiteDir, DEFAULT_PLUGIN_ID),
|
||||||
'version-1.0.0',
|
'version-1.0.0',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(versionedSidebar).toMatchSnapshot();
|
expect(versionedSidebar).toMatchSnapshot();
|
||||||
expect(versionedSidebarPath).toEqual(
|
expect(versionedSidebarPath).toEqual(
|
||||||
path.join(
|
path.join(
|
||||||
getVersionedSidebarsDir(simpleSiteDir, DEFAULT_PLUGIN_ID),
|
getVersionedSidebarsDirPath(simpleSiteDir, DEFAULT_PLUGIN_ID),
|
||||||
'version-1.0.0-sidebars.json',
|
'version-1.0.0-sidebars.json',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(versionsPath).toEqual(
|
expect(versionsPath).toEqual(
|
||||||
getVersionsJSONFile(simpleSiteDir, DEFAULT_PLUGIN_ID),
|
getVersionsFilePath(simpleSiteDir, DEFAULT_PLUGIN_ID),
|
||||||
);
|
);
|
||||||
expect(versions).toEqual(['1.0.0']);
|
expect(versions).toEqual(['1.0.0']);
|
||||||
expect(consoleMock).toHaveBeenCalledWith('[docs] Version 1.0.0 created!');
|
expect(consoleMock).toHaveBeenCalledWith('[docs] Version 1.0.0 created!');
|
||||||
|
@ -207,23 +247,28 @@ describe('docsVersion', () => {
|
||||||
path: 'docs',
|
path: 'docs',
|
||||||
sidebarPath: path.join(versionedSiteDir, 'sidebars.json'),
|
sidebarPath: path.join(versionedSiteDir, 'sidebars.json'),
|
||||||
};
|
};
|
||||||
docsVersion('2.0.0', versionedSiteDir, DEFAULT_PLUGIN_ID, options);
|
cliDocsVersionCommand(
|
||||||
|
'2.0.0',
|
||||||
|
versionedSiteDir,
|
||||||
|
DEFAULT_PLUGIN_ID,
|
||||||
|
options,
|
||||||
|
);
|
||||||
expect(copyMock).toHaveBeenCalledWith(
|
expect(copyMock).toHaveBeenCalledWith(
|
||||||
path.join(versionedSiteDir, options.path),
|
path.join(versionedSiteDir, options.path),
|
||||||
path.join(
|
path.join(
|
||||||
getVersionedDocsDir(versionedSiteDir, DEFAULT_PLUGIN_ID),
|
getVersionedDocsDirPath(versionedSiteDir, DEFAULT_PLUGIN_ID),
|
||||||
'version-2.0.0',
|
'version-2.0.0',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(versionedSidebar).toMatchSnapshot();
|
expect(versionedSidebar).toMatchSnapshot();
|
||||||
expect(versionedSidebarPath).toEqual(
|
expect(versionedSidebarPath).toEqual(
|
||||||
path.join(
|
path.join(
|
||||||
getVersionedSidebarsDir(versionedSiteDir, DEFAULT_PLUGIN_ID),
|
getVersionedSidebarsDirPath(versionedSiteDir, DEFAULT_PLUGIN_ID),
|
||||||
'version-2.0.0-sidebars.json',
|
'version-2.0.0-sidebars.json',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(versionsPath).toEqual(
|
expect(versionsPath).toEqual(
|
||||||
getVersionsJSONFile(versionedSiteDir, DEFAULT_PLUGIN_ID),
|
getVersionsFilePath(versionedSiteDir, DEFAULT_PLUGIN_ID),
|
||||||
);
|
);
|
||||||
expect(versions).toEqual(['2.0.0', '1.0.1', '1.0.0', 'withSlugs']);
|
expect(versions).toEqual(['2.0.0', '1.0.1', '1.0.0', 'withSlugs']);
|
||||||
expect(consoleMock).toHaveBeenCalledWith('[docs] Version 2.0.0 created!');
|
expect(consoleMock).toHaveBeenCalledWith('[docs] Version 2.0.0 created!');
|
||||||
|
@ -257,23 +302,23 @@ describe('docsVersion', () => {
|
||||||
path: 'community',
|
path: 'community',
|
||||||
sidebarPath: path.join(versionedSiteDir, 'community_sidebars.json'),
|
sidebarPath: path.join(versionedSiteDir, 'community_sidebars.json'),
|
||||||
};
|
};
|
||||||
docsVersion('2.0.0', versionedSiteDir, pluginId, options);
|
cliDocsVersionCommand('2.0.0', versionedSiteDir, pluginId, options);
|
||||||
expect(copyMock).toHaveBeenCalledWith(
|
expect(copyMock).toHaveBeenCalledWith(
|
||||||
path.join(versionedSiteDir, options.path),
|
path.join(versionedSiteDir, options.path),
|
||||||
path.join(
|
path.join(
|
||||||
getVersionedDocsDir(versionedSiteDir, pluginId),
|
getVersionedDocsDirPath(versionedSiteDir, pluginId),
|
||||||
'version-2.0.0',
|
'version-2.0.0',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(versionedSidebar).toMatchSnapshot();
|
expect(versionedSidebar).toMatchSnapshot();
|
||||||
expect(versionedSidebarPath).toEqual(
|
expect(versionedSidebarPath).toEqual(
|
||||||
path.join(
|
path.join(
|
||||||
getVersionedSidebarsDir(versionedSiteDir, pluginId),
|
getVersionedSidebarsDirPath(versionedSiteDir, pluginId),
|
||||||
'version-2.0.0-sidebars.json',
|
'version-2.0.0-sidebars.json',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(versionsPath).toEqual(
|
expect(versionsPath).toEqual(
|
||||||
getVersionsJSONFile(versionedSiteDir, pluginId),
|
getVersionsFilePath(versionedSiteDir, pluginId),
|
||||||
);
|
);
|
||||||
expect(versions).toEqual(['2.0.0', '1.0.0']);
|
expect(versions).toEqual(['2.0.0', '1.0.0']);
|
||||||
expect(consoleMock).toHaveBeenCalledWith(
|
expect(consoleMock).toHaveBeenCalledWith(
|
|
@ -0,0 +1,529 @@
|
||||||
|
/**
|
||||||
|
* 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 path from 'path';
|
||||||
|
import {loadContext} from '@docusaurus/core/src/server/index';
|
||||||
|
import {processDocMetadata, readVersionDocs, readDocFile} from '../docs';
|
||||||
|
import {readVersionsMetadata} from '../versions';
|
||||||
|
import {
|
||||||
|
DocFile,
|
||||||
|
DocMetadataBase,
|
||||||
|
MetadataOptions,
|
||||||
|
VersionMetadata,
|
||||||
|
} from '../types';
|
||||||
|
import {LoadContext} from '@docusaurus/types';
|
||||||
|
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
|
||||||
|
import {DEFAULT_OPTIONS} from '../options';
|
||||||
|
import {Optional} from 'utility-types';
|
||||||
|
|
||||||
|
const fixtureDir = path.join(__dirname, '__fixtures__');
|
||||||
|
|
||||||
|
const createFakeDocFile = ({
|
||||||
|
source,
|
||||||
|
frontmatter = {},
|
||||||
|
markdown = 'some markdown content',
|
||||||
|
}: {
|
||||||
|
source: string;
|
||||||
|
frontmatter?: Record<string, string>;
|
||||||
|
markdown?: string;
|
||||||
|
}): DocFile => {
|
||||||
|
const content = `---
|
||||||
|
${Object.entries(frontmatter)
|
||||||
|
.map(([key, value]) => `${key}: ${value}`)
|
||||||
|
.join('\n')}
|
||||||
|
---
|
||||||
|
${markdown}
|
||||||
|
`;
|
||||||
|
return {
|
||||||
|
source,
|
||||||
|
content,
|
||||||
|
lastUpdate: {},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function createTestUtils({
|
||||||
|
siteDir,
|
||||||
|
context,
|
||||||
|
versionMetadata,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
siteDir: string;
|
||||||
|
context: LoadContext;
|
||||||
|
versionMetadata: VersionMetadata;
|
||||||
|
options: MetadataOptions;
|
||||||
|
}) {
|
||||||
|
async function readDoc(docFileSource: string) {
|
||||||
|
return readDocFile(versionMetadata.docsDirPath, docFileSource, options);
|
||||||
|
}
|
||||||
|
function processDocFile(docFile: DocFile) {
|
||||||
|
return processDocMetadata({
|
||||||
|
docFile,
|
||||||
|
versionMetadata,
|
||||||
|
options,
|
||||||
|
context,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function testMeta(
|
||||||
|
docFileSource: string,
|
||||||
|
expectedMetadata: Optional<
|
||||||
|
DocMetadataBase,
|
||||||
|
'source' | 'lastUpdatedBy' | 'lastUpdatedAt' | 'sidebar_label' | 'editUrl'
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
const docFile = await readDoc(docFileSource);
|
||||||
|
const metadata = await processDocMetadata({
|
||||||
|
docFile,
|
||||||
|
versionMetadata,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
});
|
||||||
|
expect(metadata).toEqual({
|
||||||
|
lastUpdatedBy: undefined,
|
||||||
|
lastUpdatedAt: undefined,
|
||||||
|
sidebar_label: undefined,
|
||||||
|
editUrl: undefined,
|
||||||
|
source: path.join(
|
||||||
|
'@site',
|
||||||
|
path.relative(siteDir, versionMetadata.docsDirPath),
|
||||||
|
docFileSource,
|
||||||
|
),
|
||||||
|
...expectedMetadata,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testSlug(docFileSource: string, expectedPermalink: string) {
|
||||||
|
const docFile = await readDoc(docFileSource);
|
||||||
|
const metadata = await processDocMetadata({
|
||||||
|
docFile,
|
||||||
|
versionMetadata,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
});
|
||||||
|
expect(metadata.permalink).toEqual(expectedPermalink);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {processDocFile, testMeta, testSlug};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('simple site', () => {
|
||||||
|
const siteDir = path.join(fixtureDir, 'simple-site');
|
||||||
|
const context = loadContext(siteDir);
|
||||||
|
const options = {
|
||||||
|
id: DEFAULT_PLUGIN_ID,
|
||||||
|
...DEFAULT_OPTIONS,
|
||||||
|
};
|
||||||
|
const versionsMetadata = readVersionsMetadata({
|
||||||
|
context,
|
||||||
|
options: {
|
||||||
|
id: DEFAULT_PLUGIN_ID,
|
||||||
|
...DEFAULT_OPTIONS,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(versionsMetadata.length).toEqual(1);
|
||||||
|
const [currentVersion] = versionsMetadata;
|
||||||
|
|
||||||
|
const defaultTestUtils = createTestUtils({
|
||||||
|
siteDir,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
versionMetadata: currentVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
test('readVersionDocs', async () => {
|
||||||
|
const docs = await readVersionDocs(currentVersion, options);
|
||||||
|
expect(docs.map((doc) => doc.source)).toMatchObject([
|
||||||
|
'hello.md',
|
||||||
|
'ipsum.md',
|
||||||
|
'lorem.md',
|
||||||
|
'rootAbsoluteSlug.md',
|
||||||
|
'rootRelativeSlug.md',
|
||||||
|
'rootResolvedSlug.md',
|
||||||
|
'rootTryToEscapeSlug.md',
|
||||||
|
'foo/bar.md',
|
||||||
|
'foo/baz.md',
|
||||||
|
'slugs/absoluteSlug.md',
|
||||||
|
'slugs/relativeSlug.md',
|
||||||
|
'slugs/resolvedSlug.md',
|
||||||
|
'slugs/tryToEscapeSlug.md',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normal docs', async () => {
|
||||||
|
await defaultTestUtils.testMeta(path.join('foo', 'bar.md'), {
|
||||||
|
version: 'current',
|
||||||
|
id: 'foo/bar',
|
||||||
|
unversionedId: 'foo/bar',
|
||||||
|
isDocsHomePage: false,
|
||||||
|
permalink: '/docs/foo/bar',
|
||||||
|
slug: '/foo/bar',
|
||||||
|
title: 'Bar',
|
||||||
|
description: 'This is custom description',
|
||||||
|
});
|
||||||
|
await defaultTestUtils.testMeta(path.join('hello.md'), {
|
||||||
|
version: 'current',
|
||||||
|
id: 'hello',
|
||||||
|
unversionedId: 'hello',
|
||||||
|
isDocsHomePage: false,
|
||||||
|
permalink: '/docs/hello',
|
||||||
|
slug: '/hello',
|
||||||
|
title: 'Hello, World !',
|
||||||
|
description: `Hi, Endilie here :)`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('homePageId doc', async () => {
|
||||||
|
const testUtilsLocal = createTestUtils({
|
||||||
|
siteDir,
|
||||||
|
context,
|
||||||
|
options: {...options, homePageId: 'hello'},
|
||||||
|
versionMetadata: currentVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
await testUtilsLocal.testMeta(path.join('hello.md'), {
|
||||||
|
version: 'current',
|
||||||
|
id: 'hello',
|
||||||
|
unversionedId: 'hello',
|
||||||
|
isDocsHomePage: true,
|
||||||
|
permalink: '/docs/',
|
||||||
|
slug: '/',
|
||||||
|
title: 'Hello, World !',
|
||||||
|
description: `Hi, Endilie here :)`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('homePageId doc nested', async () => {
|
||||||
|
const testUtilsLocal = createTestUtils({
|
||||||
|
siteDir,
|
||||||
|
context,
|
||||||
|
options: {...options, homePageId: 'foo/bar'},
|
||||||
|
versionMetadata: currentVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
await testUtilsLocal.testMeta(path.join('foo', 'bar.md'), {
|
||||||
|
version: 'current',
|
||||||
|
id: 'foo/bar',
|
||||||
|
unversionedId: 'foo/bar',
|
||||||
|
isDocsHomePage: true,
|
||||||
|
permalink: '/docs/',
|
||||||
|
slug: '/',
|
||||||
|
title: 'Bar',
|
||||||
|
description: 'This is custom description',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('docs with editUrl', async () => {
|
||||||
|
const testUtilsLocal = createTestUtils({
|
||||||
|
siteDir,
|
||||||
|
context,
|
||||||
|
options: {
|
||||||
|
...options,
|
||||||
|
editUrl: 'https://github.com/facebook/docusaurus/edit/master/website',
|
||||||
|
},
|
||||||
|
versionMetadata: currentVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
await testUtilsLocal.testMeta(path.join('foo', 'baz.md'), {
|
||||||
|
version: 'current',
|
||||||
|
id: 'foo/baz',
|
||||||
|
unversionedId: 'foo/baz',
|
||||||
|
isDocsHomePage: false,
|
||||||
|
permalink: '/docs/foo/bazSlug.html',
|
||||||
|
slug: '/foo/bazSlug.html',
|
||||||
|
title: 'baz',
|
||||||
|
editUrl:
|
||||||
|
'https://github.com/facebook/docusaurus/edit/master/website/docs/foo/baz.md',
|
||||||
|
description: 'Images',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('docs with custom editUrl & unrelated frontmatter', async () => {
|
||||||
|
await defaultTestUtils.testMeta('lorem.md', {
|
||||||
|
version: 'current',
|
||||||
|
id: 'lorem',
|
||||||
|
unversionedId: 'lorem',
|
||||||
|
isDocsHomePage: false,
|
||||||
|
permalink: '/docs/lorem',
|
||||||
|
slug: '/lorem',
|
||||||
|
title: 'lorem',
|
||||||
|
editUrl: 'https://github.com/customUrl/docs/lorem.md',
|
||||||
|
description: 'Lorem ipsum.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('docs with last update time and author', async () => {
|
||||||
|
const testUtilsLocal = createTestUtils({
|
||||||
|
siteDir,
|
||||||
|
context,
|
||||||
|
options: {
|
||||||
|
...options,
|
||||||
|
showLastUpdateAuthor: true,
|
||||||
|
showLastUpdateTime: true,
|
||||||
|
},
|
||||||
|
versionMetadata: currentVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
await testUtilsLocal.testMeta('lorem.md', {
|
||||||
|
version: 'current',
|
||||||
|
id: 'lorem',
|
||||||
|
unversionedId: 'lorem',
|
||||||
|
isDocsHomePage: false,
|
||||||
|
permalink: '/docs/lorem',
|
||||||
|
slug: '/lorem',
|
||||||
|
title: 'lorem',
|
||||||
|
editUrl: 'https://github.com/customUrl/docs/lorem.md',
|
||||||
|
description: 'Lorem ipsum.',
|
||||||
|
lastUpdatedAt: 1539502055,
|
||||||
|
lastUpdatedBy: 'Author',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('docs with slugs', async () => {
|
||||||
|
await defaultTestUtils.testSlug(
|
||||||
|
path.join('rootRelativeSlug.md'),
|
||||||
|
'/docs/rootRelativeSlug',
|
||||||
|
);
|
||||||
|
await defaultTestUtils.testSlug(
|
||||||
|
path.join('rootAbsoluteSlug.md'),
|
||||||
|
'/docs/rootAbsoluteSlug',
|
||||||
|
);
|
||||||
|
await defaultTestUtils.testSlug(
|
||||||
|
path.join('rootResolvedSlug.md'),
|
||||||
|
'/docs/hey/rootResolvedSlug',
|
||||||
|
);
|
||||||
|
await defaultTestUtils.testSlug(
|
||||||
|
path.join('rootTryToEscapeSlug.md'),
|
||||||
|
'/docs/rootTryToEscapeSlug',
|
||||||
|
);
|
||||||
|
|
||||||
|
await defaultTestUtils.testSlug(
|
||||||
|
path.join('slugs', 'absoluteSlug.md'),
|
||||||
|
'/docs/absoluteSlug',
|
||||||
|
);
|
||||||
|
await defaultTestUtils.testSlug(
|
||||||
|
path.join('slugs', 'relativeSlug.md'),
|
||||||
|
'/docs/slugs/relativeSlug',
|
||||||
|
);
|
||||||
|
await defaultTestUtils.testSlug(
|
||||||
|
path.join('slugs', 'resolvedSlug.md'),
|
||||||
|
'/docs/slugs/hey/resolvedSlug',
|
||||||
|
);
|
||||||
|
await defaultTestUtils.testSlug(
|
||||||
|
path.join('slugs', 'tryToEscapeSlug.md'),
|
||||||
|
'/docs/tryToEscapeSlug',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('docs with invalid id', () => {
|
||||||
|
expect(() => {
|
||||||
|
defaultTestUtils.processDocFile(
|
||||||
|
createFakeDocFile({
|
||||||
|
source: 'some/fake/path',
|
||||||
|
frontmatter: {
|
||||||
|
id: 'Hello/world',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Document id [Hello/world] cannot include \\"/\\"."`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('docs with slug on doc home', async () => {
|
||||||
|
const testUtilsLocal = createTestUtils({
|
||||||
|
siteDir,
|
||||||
|
context,
|
||||||
|
options: {
|
||||||
|
...options,
|
||||||
|
homePageId: 'homePageId',
|
||||||
|
},
|
||||||
|
versionMetadata: currentVersion,
|
||||||
|
});
|
||||||
|
expect(() => {
|
||||||
|
testUtilsLocal.processDocFile(
|
||||||
|
createFakeDocFile({
|
||||||
|
source: 'homePageId',
|
||||||
|
frontmatter: {
|
||||||
|
slug: '/x/y',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"The docs homepage (homePageId=homePageId) is not allowed to have a frontmatter slug=/x/y => you have to chooser either homePageId or slug, not both"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('versioned site', () => {
|
||||||
|
const siteDir = path.join(fixtureDir, 'versioned-site');
|
||||||
|
const context = loadContext(siteDir);
|
||||||
|
const options = {
|
||||||
|
id: DEFAULT_PLUGIN_ID,
|
||||||
|
...DEFAULT_OPTIONS,
|
||||||
|
};
|
||||||
|
const versionsMetadata = readVersionsMetadata({
|
||||||
|
context,
|
||||||
|
options: {
|
||||||
|
id: DEFAULT_PLUGIN_ID,
|
||||||
|
...DEFAULT_OPTIONS,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(versionsMetadata.length).toEqual(4);
|
||||||
|
const [
|
||||||
|
currentVersion,
|
||||||
|
version101,
|
||||||
|
version100,
|
||||||
|
versionWithSlugs,
|
||||||
|
] = versionsMetadata;
|
||||||
|
|
||||||
|
const currentVersionTestUtils = createTestUtils({
|
||||||
|
siteDir,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
versionMetadata: currentVersion,
|
||||||
|
});
|
||||||
|
const version101TestUtils = createTestUtils({
|
||||||
|
siteDir,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
versionMetadata: version101,
|
||||||
|
});
|
||||||
|
|
||||||
|
const version100TestUtils = createTestUtils({
|
||||||
|
siteDir,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
versionMetadata: version100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const versionWithSlugsTestUtils = createTestUtils({
|
||||||
|
siteDir,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
versionMetadata: versionWithSlugs,
|
||||||
|
});
|
||||||
|
|
||||||
|
test('next docs', async () => {
|
||||||
|
await currentVersionTestUtils.testMeta(path.join('foo', 'bar.md'), {
|
||||||
|
id: 'foo/bar',
|
||||||
|
unversionedId: 'foo/bar',
|
||||||
|
isDocsHomePage: false,
|
||||||
|
permalink: '/docs/next/foo/barSlug',
|
||||||
|
slug: '/foo/barSlug',
|
||||||
|
title: 'bar',
|
||||||
|
description: 'This is next version of bar.',
|
||||||
|
version: 'current',
|
||||||
|
});
|
||||||
|
await currentVersionTestUtils.testMeta(path.join('hello.md'), {
|
||||||
|
id: 'hello',
|
||||||
|
unversionedId: 'hello',
|
||||||
|
isDocsHomePage: false,
|
||||||
|
permalink: '/docs/next/hello',
|
||||||
|
slug: '/hello',
|
||||||
|
title: 'hello',
|
||||||
|
description: 'Hello next !',
|
||||||
|
version: 'current',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('versioned docs', async () => {
|
||||||
|
await version100TestUtils.testMeta(path.join('foo', 'bar.md'), {
|
||||||
|
id: 'version-1.0.0/foo/bar',
|
||||||
|
unversionedId: 'foo/bar',
|
||||||
|
isDocsHomePage: false,
|
||||||
|
permalink: '/docs/1.0.0/foo/barSlug',
|
||||||
|
slug: '/foo/barSlug',
|
||||||
|
title: 'bar',
|
||||||
|
description: 'Bar 1.0.0 !',
|
||||||
|
version: '1.0.0',
|
||||||
|
});
|
||||||
|
await version100TestUtils.testMeta(path.join('hello.md'), {
|
||||||
|
id: 'version-1.0.0/hello',
|
||||||
|
unversionedId: 'hello',
|
||||||
|
isDocsHomePage: false,
|
||||||
|
permalink: '/docs/1.0.0/hello',
|
||||||
|
slug: '/hello',
|
||||||
|
title: 'hello',
|
||||||
|
description: 'Hello 1.0.0 !',
|
||||||
|
version: '1.0.0',
|
||||||
|
});
|
||||||
|
await version101TestUtils.testMeta(path.join('foo', 'bar.md'), {
|
||||||
|
id: 'version-1.0.1/foo/bar',
|
||||||
|
unversionedId: 'foo/bar',
|
||||||
|
isDocsHomePage: false,
|
||||||
|
permalink: '/docs/foo/bar',
|
||||||
|
slug: '/foo/bar',
|
||||||
|
title: 'bar',
|
||||||
|
description: 'Bar 1.0.1 !',
|
||||||
|
version: '1.0.1',
|
||||||
|
});
|
||||||
|
await version101TestUtils.testMeta(path.join('hello.md'), {
|
||||||
|
id: 'version-1.0.1/hello',
|
||||||
|
unversionedId: 'hello',
|
||||||
|
isDocsHomePage: false,
|
||||||
|
permalink: '/docs/hello',
|
||||||
|
slug: '/hello',
|
||||||
|
title: 'hello',
|
||||||
|
description: 'Hello 1.0.1 !',
|
||||||
|
version: '1.0.1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('next doc slugs', async () => {
|
||||||
|
await currentVersionTestUtils.testSlug(
|
||||||
|
path.join('slugs', 'absoluteSlug.md'),
|
||||||
|
'/docs/next/absoluteSlug',
|
||||||
|
);
|
||||||
|
await currentVersionTestUtils.testSlug(
|
||||||
|
path.join('slugs', 'relativeSlug.md'),
|
||||||
|
'/docs/next/slugs/relativeSlug',
|
||||||
|
);
|
||||||
|
await currentVersionTestUtils.testSlug(
|
||||||
|
path.join('slugs', 'resolvedSlug.md'),
|
||||||
|
'/docs/next/slugs/hey/resolvedSlug',
|
||||||
|
);
|
||||||
|
await currentVersionTestUtils.testSlug(
|
||||||
|
path.join('slugs', 'tryToEscapeSlug.md'),
|
||||||
|
'/docs/next/tryToEscapeSlug',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('versioned doc slugs', async () => {
|
||||||
|
await versionWithSlugsTestUtils.testSlug(
|
||||||
|
path.join('rootAbsoluteSlug.md'),
|
||||||
|
'/docs/withSlugs/rootAbsoluteSlug',
|
||||||
|
);
|
||||||
|
await versionWithSlugsTestUtils.testSlug(
|
||||||
|
path.join('rootRelativeSlug.md'),
|
||||||
|
'/docs/withSlugs/rootRelativeSlug',
|
||||||
|
);
|
||||||
|
await versionWithSlugsTestUtils.testSlug(
|
||||||
|
path.join('rootResolvedSlug.md'),
|
||||||
|
'/docs/withSlugs/hey/rootResolvedSlug',
|
||||||
|
);
|
||||||
|
await versionWithSlugsTestUtils.testSlug(
|
||||||
|
path.join('rootTryToEscapeSlug.md'),
|
||||||
|
'/docs/withSlugs/rootTryToEscapeSlug',
|
||||||
|
);
|
||||||
|
|
||||||
|
await versionWithSlugsTestUtils.testSlug(
|
||||||
|
path.join('slugs', 'absoluteSlug.md'),
|
||||||
|
'/docs/withSlugs/absoluteSlug',
|
||||||
|
);
|
||||||
|
await versionWithSlugsTestUtils.testSlug(
|
||||||
|
path.join('slugs', 'relativeSlug.md'),
|
||||||
|
'/docs/withSlugs/slugs/relativeSlug',
|
||||||
|
);
|
||||||
|
await versionWithSlugsTestUtils.testSlug(
|
||||||
|
path.join('slugs', 'resolvedSlug.md'),
|
||||||
|
'/docs/withSlugs/slugs/hey/resolvedSlug',
|
||||||
|
);
|
||||||
|
await versionWithSlugsTestUtils.testSlug(
|
||||||
|
path.join('slugs', 'tryToEscapeSlug.md'),
|
||||||
|
'/docs/withSlugs/tryToEscapeSlug',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,58 +0,0 @@
|
||||||
/**
|
|
||||||
* 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 path from 'path';
|
|
||||||
import loadEnv from '../env';
|
|
||||||
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
|
|
||||||
|
|
||||||
describe('loadEnv', () => {
|
|
||||||
test('website with versioning disabled', () => {
|
|
||||||
const siteDir = path.join(__dirname, '__fixtures__', 'simple-site');
|
|
||||||
const env = loadEnv(siteDir, DEFAULT_PLUGIN_ID);
|
|
||||||
expect(env.versioning.enabled).toBe(false);
|
|
||||||
expect(env.versioning.versions).toStrictEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('website with versioning enabled', () => {
|
|
||||||
const siteDir = path.join(__dirname, '__fixtures__', 'versioned-site');
|
|
||||||
const env = loadEnv(siteDir, DEFAULT_PLUGIN_ID);
|
|
||||||
expect(env.versioning.enabled).toBe(true);
|
|
||||||
expect(env.versioning.latestVersion).toBe('1.0.1');
|
|
||||||
expect(env.versioning.versions).toStrictEqual([
|
|
||||||
'1.0.1',
|
|
||||||
'1.0.0',
|
|
||||||
'withSlugs',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('website with versioning enabled, 2nd docs plugin instance', () => {
|
|
||||||
const siteDir = path.join(__dirname, '__fixtures__', 'versioned-site');
|
|
||||||
const env = loadEnv(siteDir, 'community');
|
|
||||||
expect(env.versioning.enabled).toBe(true);
|
|
||||||
expect(env.versioning.latestVersion).toBe('1.0.0');
|
|
||||||
expect(env.versioning.versions).toStrictEqual(['1.0.0']);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('website with versioning but disabled', () => {
|
|
||||||
const siteDir = path.join(__dirname, '__fixtures__', 'versioned-site');
|
|
||||||
const env = loadEnv(siteDir, DEFAULT_PLUGIN_ID, {disableVersioning: true});
|
|
||||||
expect(env.versioning.enabled).toBe(false);
|
|
||||||
expect(env.versioning.versions).toStrictEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('website with invalid versions.json file', () => {
|
|
||||||
const siteDir = path.join(__dirname, '__fixtures__', 'versioned-site');
|
|
||||||
const mock = jest.spyOn(JSON, 'parse').mockImplementationOnce(() => {
|
|
||||||
return {
|
|
||||||
invalid: 'json',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const env = loadEnv(siteDir, DEFAULT_PLUGIN_ID);
|
|
||||||
expect(env.versioning.enabled).toBe(false);
|
|
||||||
mock.mockRestore();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -6,12 +6,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {validate} from 'webpack';
|
|
||||||
import {isMatch} from 'picomatch';
|
import {isMatch} from 'picomatch';
|
||||||
import commander from 'commander';
|
import commander from 'commander';
|
||||||
|
import {kebabCase} from 'lodash';
|
||||||
|
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import pluginContentDocs from '../index';
|
import pluginContentDocs from '../index';
|
||||||
import loadEnv from '../env';
|
|
||||||
import {loadContext} from '@docusaurus/core/src/server/index';
|
import {loadContext} from '@docusaurus/core/src/server/index';
|
||||||
import {applyConfigureWebpack} from '@docusaurus/core/src/webpack/utils';
|
import {applyConfigureWebpack} from '@docusaurus/core/src/webpack/utils';
|
||||||
import {RouteConfig} from '@docusaurus/types';
|
import {RouteConfig} from '@docusaurus/types';
|
||||||
|
@ -19,9 +19,26 @@ import {posixPath} from '@docusaurus/utils';
|
||||||
import {sortConfig} from '@docusaurus/core/src/server/plugins';
|
import {sortConfig} from '@docusaurus/core/src/server/plugins';
|
||||||
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
|
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
|
||||||
|
|
||||||
import * as version from '../version';
|
import * as cliDocs from '../cli';
|
||||||
import {PluginOptionSchema} from '../pluginOptionSchema';
|
import {OptionsSchema} from '../options';
|
||||||
import {normalizePluginOptions} from '@docusaurus/utils-validation';
|
import {normalizePluginOptions} from '@docusaurus/utils-validation';
|
||||||
|
import {DocMetadata, LoadedVersion} from '../types';
|
||||||
|
import {toSidebarsProp} from '../props';
|
||||||
|
|
||||||
|
// @ts-expect-error: TODO typedefs missing?
|
||||||
|
import {validate} from 'webpack';
|
||||||
|
|
||||||
|
function findDocById(version: LoadedVersion, unversionedId: string) {
|
||||||
|
return version.docs.find((item) => item.unversionedId === unversionedId);
|
||||||
|
}
|
||||||
|
const defaultDocMetadata: Partial<DocMetadata> = {
|
||||||
|
next: undefined,
|
||||||
|
previous: undefined,
|
||||||
|
editUrl: undefined,
|
||||||
|
lastUpdatedAt: undefined,
|
||||||
|
lastUpdatedBy: undefined,
|
||||||
|
sidebar_label: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
const createFakeActions = (contentDir: string) => {
|
const createFakeActions = (contentDir: string) => {
|
||||||
const routeConfigs: RouteConfig[] = [];
|
const routeConfigs: RouteConfig[] = [];
|
||||||
|
@ -41,20 +58,34 @@ const createFakeActions = (contentDir: string) => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// query by prefix, because files have a hash at the end
|
||||||
|
// so it's not convenient to query by full filename
|
||||||
|
const getCreatedDataByPrefix = (prefix: string) => {
|
||||||
|
const entry = Object.entries(dataContainer).find(([key]) =>
|
||||||
|
key.startsWith(prefix),
|
||||||
|
);
|
||||||
|
if (!entry) {
|
||||||
|
throw new Error(`No created entry found for prefix=[${prefix}]
|
||||||
|
Entries created:
|
||||||
|
- ${Object.keys(dataContainer).join('\n- ')}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
return JSON.parse(entry[1] as string);
|
||||||
|
};
|
||||||
|
|
||||||
// Extra fns useful for tests!
|
// Extra fns useful for tests!
|
||||||
const utils = {
|
const utils = {
|
||||||
getGlobalData: () => globalDataContainer,
|
getGlobalData: () => globalDataContainer,
|
||||||
getRouteConfigs: () => routeConfigs,
|
getRouteConfigs: () => routeConfigs,
|
||||||
// query by prefix, because files have a hash at the end
|
|
||||||
// so it's not convenient to query by full filename
|
checkVersionMetadataPropCreated: (version: LoadedVersion) => {
|
||||||
getCreatedDataByPrefix: (prefix: string) => {
|
const versionMetadataProp = getCreatedDataByPrefix(
|
||||||
const entry = Object.entries(dataContainer).find(([key]) =>
|
`version-${kebabCase(version.versionName)}-metadata-prop`,
|
||||||
key.startsWith(prefix),
|
);
|
||||||
|
expect(versionMetadataProp.docsSidebars).toEqual(toSidebarsProp(version));
|
||||||
|
expect(versionMetadataProp.permalinkToSidebar).toEqual(
|
||||||
|
version.permalinkToSidebar,
|
||||||
);
|
);
|
||||||
if (!entry) {
|
|
||||||
throw new Error(`No entry found for prefix=${prefix}`);
|
|
||||||
}
|
|
||||||
return JSON.parse(entry[1] as string);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
expectSnapshot: () => {
|
expectSnapshot: () => {
|
||||||
|
@ -79,11 +110,11 @@ test('site with wrong sidebar file', async () => {
|
||||||
const sidebarPath = path.join(siteDir, 'wrong-sidebars.json');
|
const sidebarPath = path.join(siteDir, 'wrong-sidebars.json');
|
||||||
const plugin = pluginContentDocs(
|
const plugin = pluginContentDocs(
|
||||||
context,
|
context,
|
||||||
normalizePluginOptions(PluginOptionSchema, {
|
normalizePluginOptions(OptionsSchema, {
|
||||||
sidebarPath,
|
sidebarPath,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await expect(plugin.loadContent()).rejects.toThrowErrorMatchingSnapshot();
|
await expect(plugin.loadContent!()).rejects.toThrowErrorMatchingSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('empty/no docs website', () => {
|
describe('empty/no docs website', () => {
|
||||||
|
@ -94,33 +125,26 @@ describe('empty/no docs website', () => {
|
||||||
await fs.ensureDir(path.join(siteDir, 'docs'));
|
await fs.ensureDir(path.join(siteDir, 'docs'));
|
||||||
const plugin = pluginContentDocs(
|
const plugin = pluginContentDocs(
|
||||||
context,
|
context,
|
||||||
normalizePluginOptions(PluginOptionSchema, {}),
|
normalizePluginOptions(OptionsSchema, {}),
|
||||||
|
);
|
||||||
|
await expect(
|
||||||
|
plugin.loadContent!(),
|
||||||
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Docs version current has no docs! At least one doc should exist at path=[docs]"`,
|
||||||
);
|
);
|
||||||
const content = await plugin.loadContent();
|
|
||||||
const {docsMetadata, docsSidebars} = content;
|
|
||||||
expect(docsMetadata).toMatchInlineSnapshot(`Object {}`);
|
|
||||||
expect(docsSidebars).toMatchInlineSnapshot(`Object {}`);
|
|
||||||
|
|
||||||
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
|
|
||||||
const {actions, utils} = createFakeActions(pluginContentDir);
|
|
||||||
|
|
||||||
await plugin.contentLoaded({
|
|
||||||
content,
|
|
||||||
actions,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(utils.getRouteConfigs()).toEqual([]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('docs folder does not exist', async () => {
|
test('docs folder does not exist', async () => {
|
||||||
const plugin = pluginContentDocs(
|
expect(() =>
|
||||||
context,
|
pluginContentDocs(
|
||||||
normalizePluginOptions(PluginOptionSchema, {
|
context,
|
||||||
path: '/path/does/not/exist/',
|
normalizePluginOptions(OptionsSchema, {
|
||||||
}),
|
path: '/path/does/not/exist/',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"The docs folder does not exist for version [current]. A docs folder is expected to be found at /path/does/not/exist"`,
|
||||||
);
|
);
|
||||||
const content = await plugin.loadContent();
|
|
||||||
expect(content).toBeNull();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -128,11 +152,10 @@ describe('simple website', () => {
|
||||||
const siteDir = path.join(__dirname, '__fixtures__', 'simple-site');
|
const siteDir = path.join(__dirname, '__fixtures__', 'simple-site');
|
||||||
const context = loadContext(siteDir);
|
const context = loadContext(siteDir);
|
||||||
const sidebarPath = path.join(siteDir, 'sidebars.json');
|
const sidebarPath = path.join(siteDir, 'sidebars.json');
|
||||||
const pluginPath = 'docs';
|
|
||||||
const plugin = pluginContentDocs(
|
const plugin = pluginContentDocs(
|
||||||
context,
|
context,
|
||||||
normalizePluginOptions(PluginOptionSchema, {
|
normalizePluginOptions(OptionsSchema, {
|
||||||
path: pluginPath,
|
path: 'docs',
|
||||||
sidebarPath,
|
sidebarPath,
|
||||||
homePageId: 'hello',
|
homePageId: 'hello',
|
||||||
}),
|
}),
|
||||||
|
@ -140,27 +163,31 @@ describe('simple website', () => {
|
||||||
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
|
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
|
||||||
|
|
||||||
test('extendCli - docsVersion', () => {
|
test('extendCli - docsVersion', () => {
|
||||||
const mock = jest.spyOn(version, 'docsVersion').mockImplementation();
|
const mock = jest
|
||||||
|
.spyOn(cliDocs, 'cliDocsVersionCommand')
|
||||||
|
.mockImplementation();
|
||||||
const cli = new commander.Command();
|
const cli = new commander.Command();
|
||||||
plugin.extendCli(cli);
|
// @ts-expect-error: TODO annoying type incompatibility
|
||||||
|
plugin.extendCli!(cli);
|
||||||
cli.parse(['node', 'test', 'docs:version', '1.0.0']);
|
cli.parse(['node', 'test', 'docs:version', '1.0.0']);
|
||||||
|
expect(mock).toHaveBeenCalledTimes(1);
|
||||||
expect(mock).toHaveBeenCalledWith('1.0.0', siteDir, DEFAULT_PLUGIN_ID, {
|
expect(mock).toHaveBeenCalledWith('1.0.0', siteDir, DEFAULT_PLUGIN_ID, {
|
||||||
path: pluginPath,
|
path: 'docs',
|
||||||
sidebarPath,
|
sidebarPath,
|
||||||
});
|
});
|
||||||
mock.mockRestore();
|
mock.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getPathToWatch', () => {
|
test('getPathToWatch', () => {
|
||||||
const pathToWatch = plugin.getPathsToWatch();
|
const pathToWatch = plugin.getPathsToWatch!();
|
||||||
const matchPattern = pathToWatch.map((filepath) =>
|
const matchPattern = pathToWatch.map((filepath) =>
|
||||||
posixPath(path.relative(siteDir, filepath)),
|
posixPath(path.relative(siteDir, filepath)),
|
||||||
);
|
);
|
||||||
expect(matchPattern).not.toEqual([]);
|
expect(matchPattern).not.toEqual([]);
|
||||||
expect(matchPattern).toMatchInlineSnapshot(`
|
expect(matchPattern).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
"docs/**/*.{md,mdx}",
|
|
||||||
"sidebars.json",
|
"sidebars.json",
|
||||||
|
"docs/**/*.{md,mdx}",
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
expect(isMatch('docs/hello.md', matchPattern)).toEqual(true);
|
expect(isMatch('docs/hello.md', matchPattern)).toEqual(true);
|
||||||
|
@ -192,15 +219,13 @@ describe('simple website', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('content', async () => {
|
test('content', async () => {
|
||||||
const content = await plugin.loadContent();
|
const content = await plugin.loadContent!();
|
||||||
const {
|
expect(content.loadedVersions.length).toEqual(1);
|
||||||
docsMetadata,
|
const [currentVersion] = content.loadedVersions;
|
||||||
docsSidebars,
|
|
||||||
versionToSidebars,
|
expect(findDocById(currentVersion, 'hello')).toEqual({
|
||||||
permalinkToSidebar,
|
...defaultDocMetadata,
|
||||||
} = content;
|
version: 'current',
|
||||||
expect(versionToSidebars).toEqual({});
|
|
||||||
expect(docsMetadata.hello).toEqual({
|
|
||||||
id: 'hello',
|
id: 'hello',
|
||||||
unversionedId: 'hello',
|
unversionedId: 'hello',
|
||||||
isDocsHomePage: true,
|
isDocsHomePage: true,
|
||||||
|
@ -211,12 +236,18 @@ describe('simple website', () => {
|
||||||
permalink: '/docs/foo/bazSlug.html',
|
permalink: '/docs/foo/bazSlug.html',
|
||||||
},
|
},
|
||||||
sidebar: 'docs',
|
sidebar: 'docs',
|
||||||
source: path.join('@site', pluginPath, 'hello.md'),
|
source: path.join(
|
||||||
|
'@site',
|
||||||
|
path.relative(siteDir, currentVersion.docsDirPath),
|
||||||
|
'hello.md',
|
||||||
|
),
|
||||||
title: 'Hello, World !',
|
title: 'Hello, World !',
|
||||||
description: 'Hi, Endilie here :)',
|
description: 'Hi, Endilie here :)',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(docsMetadata['foo/bar']).toEqual({
|
expect(findDocById(currentVersion, 'foo/bar')).toEqual({
|
||||||
|
...defaultDocMetadata,
|
||||||
|
version: 'current',
|
||||||
id: 'foo/bar',
|
id: 'foo/bar',
|
||||||
unversionedId: 'foo/bar',
|
unversionedId: 'foo/bar',
|
||||||
isDocsHomePage: false,
|
isDocsHomePage: false,
|
||||||
|
@ -227,26 +258,30 @@ describe('simple website', () => {
|
||||||
permalink: '/docs/foo/bar',
|
permalink: '/docs/foo/bar',
|
||||||
slug: '/foo/bar',
|
slug: '/foo/bar',
|
||||||
sidebar: 'docs',
|
sidebar: 'docs',
|
||||||
source: path.join('@site', pluginPath, 'foo', 'bar.md'),
|
source: path.join(
|
||||||
|
'@site',
|
||||||
|
path.relative(siteDir, currentVersion.docsDirPath),
|
||||||
|
'foo',
|
||||||
|
'bar.md',
|
||||||
|
),
|
||||||
title: 'Bar',
|
title: 'Bar',
|
||||||
description: 'This is custom description',
|
description: 'This is custom description',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(docsSidebars).toMatchSnapshot();
|
expect(currentVersion.sidebars).toMatchSnapshot();
|
||||||
|
|
||||||
const {actions, utils} = createFakeActions(pluginContentDir);
|
const {actions, utils} = createFakeActions(pluginContentDir);
|
||||||
|
|
||||||
await plugin.contentLoaded({
|
await plugin.contentLoaded!({
|
||||||
content,
|
content,
|
||||||
actions,
|
actions,
|
||||||
|
allContent: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
// There is only one nested docs route for simple site
|
utils.checkVersionMetadataPropCreated(currentVersion);
|
||||||
const baseMetadata = utils.getCreatedDataByPrefix('docs-route-');
|
|
||||||
expect(baseMetadata.docsSidebars).toEqual(docsSidebars);
|
|
||||||
expect(baseMetadata.permalinkToSidebar).toEqual(permalinkToSidebar);
|
|
||||||
|
|
||||||
utils.expectSnapshot();
|
utils.expectSnapshot();
|
||||||
|
|
||||||
expect(utils.getGlobalData()).toMatchSnapshot();
|
expect(utils.getGlobalData()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -258,25 +293,24 @@ describe('versioned website', () => {
|
||||||
const routeBasePath = 'docs';
|
const routeBasePath = 'docs';
|
||||||
const plugin = pluginContentDocs(
|
const plugin = pluginContentDocs(
|
||||||
context,
|
context,
|
||||||
normalizePluginOptions(PluginOptionSchema, {
|
normalizePluginOptions(OptionsSchema, {
|
||||||
routeBasePath,
|
routeBasePath,
|
||||||
sidebarPath,
|
sidebarPath,
|
||||||
homePageId: 'hello',
|
homePageId: 'hello',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const env = loadEnv(siteDir, DEFAULT_PLUGIN_ID);
|
|
||||||
const {docsDir: versionedDir} = env.versioning;
|
|
||||||
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
|
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
|
||||||
|
|
||||||
test('isVersioned', () => {
|
|
||||||
expect(env.versioning.enabled).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('extendCli - docsVersion', () => {
|
test('extendCli - docsVersion', () => {
|
||||||
const mock = jest.spyOn(version, 'docsVersion').mockImplementation();
|
const mock = jest
|
||||||
|
.spyOn(cliDocs, 'cliDocsVersionCommand')
|
||||||
|
.mockImplementation();
|
||||||
const cli = new commander.Command();
|
const cli = new commander.Command();
|
||||||
plugin.extendCli(cli);
|
// @ts-expect-error: TODO annoying type incompatibility
|
||||||
|
plugin.extendCli!(cli);
|
||||||
cli.parse(['node', 'test', 'docs:version', '2.0.0']);
|
cli.parse(['node', 'test', 'docs:version', '2.0.0']);
|
||||||
|
expect(mock).toHaveBeenCalledTimes(1);
|
||||||
expect(mock).toHaveBeenCalledWith('2.0.0', siteDir, DEFAULT_PLUGIN_ID, {
|
expect(mock).toHaveBeenCalledWith('2.0.0', siteDir, DEFAULT_PLUGIN_ID, {
|
||||||
path: routeBasePath,
|
path: routeBasePath,
|
||||||
sidebarPath,
|
sidebarPath,
|
||||||
|
@ -285,21 +319,21 @@ describe('versioned website', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getPathToWatch', () => {
|
test('getPathToWatch', () => {
|
||||||
const pathToWatch = plugin.getPathsToWatch();
|
const pathToWatch = plugin.getPathsToWatch!();
|
||||||
const matchPattern = pathToWatch.map((filepath) =>
|
const matchPattern = pathToWatch.map((filepath) =>
|
||||||
posixPath(path.relative(siteDir, filepath)),
|
posixPath(path.relative(siteDir, filepath)),
|
||||||
);
|
);
|
||||||
expect(matchPattern).not.toEqual([]);
|
expect(matchPattern).not.toEqual([]);
|
||||||
expect(matchPattern).toMatchInlineSnapshot(`
|
expect(matchPattern).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
|
"sidebars.json",
|
||||||
"docs/**/*.{md,mdx}",
|
"docs/**/*.{md,mdx}",
|
||||||
"versioned_sidebars/version-1.0.1-sidebars.json",
|
"versioned_sidebars/version-1.0.1-sidebars.json",
|
||||||
"versioned_sidebars/version-1.0.0-sidebars.json",
|
|
||||||
"versioned_sidebars/version-withSlugs-sidebars.json",
|
|
||||||
"versioned_docs/version-1.0.1/**/*.{md,mdx}",
|
"versioned_docs/version-1.0.1/**/*.{md,mdx}",
|
||||||
|
"versioned_sidebars/version-1.0.0-sidebars.json",
|
||||||
"versioned_docs/version-1.0.0/**/*.{md,mdx}",
|
"versioned_docs/version-1.0.0/**/*.{md,mdx}",
|
||||||
|
"versioned_sidebars/version-withSlugs-sidebars.json",
|
||||||
"versioned_docs/version-withSlugs/**/*.{md,mdx}",
|
"versioned_docs/version-withSlugs/**/*.{md,mdx}",
|
||||||
"sidebars.json",
|
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
expect(isMatch('docs/hello.md', matchPattern)).toEqual(true);
|
expect(isMatch('docs/hello.md', matchPattern)).toEqual(true);
|
||||||
|
@ -335,50 +369,65 @@ describe('versioned website', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('content', async () => {
|
test('content', async () => {
|
||||||
const content = await plugin.loadContent();
|
const content = await plugin.loadContent!();
|
||||||
const {
|
expect(content.loadedVersions.length).toEqual(4);
|
||||||
docsMetadata,
|
const [
|
||||||
docsSidebars,
|
currentVersion,
|
||||||
versionToSidebars,
|
version101,
|
||||||
permalinkToSidebar,
|
version100,
|
||||||
} = content;
|
versionWithSlugs,
|
||||||
|
] = content.loadedVersions;
|
||||||
|
|
||||||
// foo/baz.md only exists in version -1.0.0
|
// foo/baz.md only exists in version -1.0.0
|
||||||
expect(docsMetadata['foo/baz']).toBeUndefined();
|
expect(findDocById(currentVersion, 'foo/baz')).toBeUndefined();
|
||||||
expect(docsMetadata['version-1.0.1/foo/baz']).toBeUndefined();
|
expect(findDocById(version101, 'foo/baz')).toBeUndefined();
|
||||||
expect(docsMetadata['foo/bar']).toEqual({
|
expect(findDocById(versionWithSlugs, 'foo/baz')).toBeUndefined();
|
||||||
|
|
||||||
|
expect(findDocById(currentVersion, 'foo/bar')).toEqual({
|
||||||
|
...defaultDocMetadata,
|
||||||
id: 'foo/bar',
|
id: 'foo/bar',
|
||||||
unversionedId: 'foo/bar',
|
unversionedId: 'foo/bar',
|
||||||
isDocsHomePage: false,
|
isDocsHomePage: false,
|
||||||
permalink: '/docs/next/foo/barSlug',
|
permalink: '/docs/next/foo/barSlug',
|
||||||
slug: '/foo/barSlug',
|
slug: '/foo/barSlug',
|
||||||
source: path.join('@site', routeBasePath, 'foo', 'bar.md'),
|
source: path.join(
|
||||||
|
'@site',
|
||||||
|
path.relative(siteDir, currentVersion.docsDirPath),
|
||||||
|
'foo',
|
||||||
|
'bar.md',
|
||||||
|
),
|
||||||
title: 'bar',
|
title: 'bar',
|
||||||
description: 'This is next version of bar.',
|
description: 'This is next version of bar.',
|
||||||
version: 'next',
|
version: 'current',
|
||||||
sidebar: 'docs',
|
sidebar: 'docs',
|
||||||
next: {
|
next: {
|
||||||
title: 'hello',
|
title: 'hello',
|
||||||
permalink: '/docs/next/',
|
permalink: '/docs/next/',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(docsMetadata.hello).toEqual({
|
expect(findDocById(currentVersion, 'hello')).toEqual({
|
||||||
|
...defaultDocMetadata,
|
||||||
id: 'hello',
|
id: 'hello',
|
||||||
unversionedId: 'hello',
|
unversionedId: 'hello',
|
||||||
isDocsHomePage: true,
|
isDocsHomePage: true,
|
||||||
permalink: '/docs/next/',
|
permalink: '/docs/next/',
|
||||||
slug: '/',
|
slug: '/',
|
||||||
source: path.join('@site', routeBasePath, 'hello.md'),
|
source: path.join(
|
||||||
|
'@site',
|
||||||
|
path.relative(siteDir, currentVersion.docsDirPath),
|
||||||
|
'hello.md',
|
||||||
|
),
|
||||||
title: 'hello',
|
title: 'hello',
|
||||||
description: 'Hello next !',
|
description: 'Hello next !',
|
||||||
version: 'next',
|
version: 'current',
|
||||||
sidebar: 'docs',
|
sidebar: 'docs',
|
||||||
previous: {
|
previous: {
|
||||||
title: 'bar',
|
title: 'bar',
|
||||||
permalink: '/docs/next/foo/barSlug',
|
permalink: '/docs/next/foo/barSlug',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(docsMetadata['version-1.0.1/hello']).toEqual({
|
expect(findDocById(version101, 'hello')).toEqual({
|
||||||
|
...defaultDocMetadata,
|
||||||
id: 'version-1.0.1/hello',
|
id: 'version-1.0.1/hello',
|
||||||
unversionedId: 'hello',
|
unversionedId: 'hello',
|
||||||
isDocsHomePage: true,
|
isDocsHomePage: true,
|
||||||
|
@ -386,8 +435,7 @@ describe('versioned website', () => {
|
||||||
slug: '/',
|
slug: '/',
|
||||||
source: path.join(
|
source: path.join(
|
||||||
'@site',
|
'@site',
|
||||||
path.relative(siteDir, versionedDir),
|
path.relative(siteDir, version101.docsDirPath),
|
||||||
'version-1.0.1',
|
|
||||||
'hello.md',
|
'hello.md',
|
||||||
),
|
),
|
||||||
title: 'hello',
|
title: 'hello',
|
||||||
|
@ -399,7 +447,8 @@ describe('versioned website', () => {
|
||||||
permalink: '/docs/foo/bar',
|
permalink: '/docs/foo/bar',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(docsMetadata['version-1.0.0/foo/baz']).toEqual({
|
expect(findDocById(version100, 'foo/baz')).toEqual({
|
||||||
|
...defaultDocMetadata,
|
||||||
id: 'version-1.0.0/foo/baz',
|
id: 'version-1.0.0/foo/baz',
|
||||||
unversionedId: 'foo/baz',
|
unversionedId: 'foo/baz',
|
||||||
isDocsHomePage: false,
|
isDocsHomePage: false,
|
||||||
|
@ -407,8 +456,7 @@ describe('versioned website', () => {
|
||||||
slug: '/foo/baz',
|
slug: '/foo/baz',
|
||||||
source: path.join(
|
source: path.join(
|
||||||
'@site',
|
'@site',
|
||||||
path.relative(siteDir, versionedDir),
|
path.relative(siteDir, version100.docsDirPath),
|
||||||
'version-1.0.0',
|
|
||||||
'foo',
|
'foo',
|
||||||
'baz.md',
|
'baz.md',
|
||||||
),
|
),
|
||||||
|
@ -427,47 +475,24 @@ describe('versioned website', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(docsSidebars).toMatchSnapshot('all sidebars');
|
expect(currentVersion.sidebars).toMatchSnapshot('current version sidebars');
|
||||||
expect(versionToSidebars).toMatchSnapshot(
|
expect(version101.sidebars).toMatchSnapshot('101 version sidebars');
|
||||||
'sidebars needed for each version',
|
expect(version100.sidebars).toMatchSnapshot('100 version sidebars');
|
||||||
|
expect(versionWithSlugs.sidebars).toMatchSnapshot(
|
||||||
|
'withSlugs version sidebars',
|
||||||
);
|
);
|
||||||
|
|
||||||
const {actions, utils} = createFakeActions(pluginContentDir);
|
const {actions, utils} = createFakeActions(pluginContentDir);
|
||||||
await plugin.contentLoaded({
|
await plugin.contentLoaded!({
|
||||||
content,
|
content,
|
||||||
actions,
|
actions,
|
||||||
|
allContent: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
// The created base metadata for each nested docs route is smartly chunked/ splitted across version
|
utils.checkVersionMetadataPropCreated(currentVersion);
|
||||||
const latestVersionBaseMetadata = utils.getCreatedDataByPrefix(
|
utils.checkVersionMetadataPropCreated(version101);
|
||||||
'docs-route-',
|
utils.checkVersionMetadataPropCreated(version100);
|
||||||
);
|
utils.checkVersionMetadataPropCreated(versionWithSlugs);
|
||||||
expect(latestVersionBaseMetadata).toMatchSnapshot(
|
|
||||||
'base metadata for latest version',
|
|
||||||
);
|
|
||||||
expect(latestVersionBaseMetadata.docsSidebars).not.toEqual(docsSidebars);
|
|
||||||
expect(latestVersionBaseMetadata.permalinkToSidebar).not.toEqual(
|
|
||||||
permalinkToSidebar,
|
|
||||||
);
|
|
||||||
const nextVersionBaseMetadata = utils.getCreatedDataByPrefix(
|
|
||||||
'docs-next-route-',
|
|
||||||
);
|
|
||||||
expect(nextVersionBaseMetadata).toMatchSnapshot(
|
|
||||||
'base metadata for next version',
|
|
||||||
);
|
|
||||||
expect(nextVersionBaseMetadata.docsSidebars).not.toEqual(docsSidebars);
|
|
||||||
expect(nextVersionBaseMetadata.permalinkToSidebar).not.toEqual(
|
|
||||||
permalinkToSidebar,
|
|
||||||
);
|
|
||||||
const firstVersionBaseMetadata = utils.getCreatedDataByPrefix(
|
|
||||||
'docs-1-0-0-route-',
|
|
||||||
);
|
|
||||||
expect(firstVersionBaseMetadata).toMatchSnapshot(
|
|
||||||
'base metadata for first version',
|
|
||||||
);
|
|
||||||
expect(nextVersionBaseMetadata.docsSidebars).not.toEqual(docsSidebars);
|
|
||||||
expect(nextVersionBaseMetadata.permalinkToSidebar).not.toEqual(
|
|
||||||
permalinkToSidebar,
|
|
||||||
);
|
|
||||||
|
|
||||||
utils.expectSnapshot();
|
utils.expectSnapshot();
|
||||||
});
|
});
|
||||||
|
@ -481,26 +506,24 @@ describe('versioned website (community)', () => {
|
||||||
const pluginId = 'community';
|
const pluginId = 'community';
|
||||||
const plugin = pluginContentDocs(
|
const plugin = pluginContentDocs(
|
||||||
context,
|
context,
|
||||||
normalizePluginOptions(PluginOptionSchema, {
|
normalizePluginOptions(OptionsSchema, {
|
||||||
id: 'community',
|
id: 'community',
|
||||||
path: 'community',
|
path: 'community',
|
||||||
routeBasePath,
|
routeBasePath,
|
||||||
sidebarPath,
|
sidebarPath,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const env = loadEnv(siteDir, pluginId);
|
|
||||||
const {docsDir: versionedDir} = env.versioning;
|
|
||||||
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
|
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
|
||||||
|
|
||||||
test('isVersioned', () => {
|
|
||||||
expect(env.versioning.enabled).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('extendCli - docsVersion', () => {
|
test('extendCli - docsVersion', () => {
|
||||||
const mock = jest.spyOn(version, 'docsVersion').mockImplementation();
|
const mock = jest
|
||||||
|
.spyOn(cliDocs, 'cliDocsVersionCommand')
|
||||||
|
.mockImplementation();
|
||||||
const cli = new commander.Command();
|
const cli = new commander.Command();
|
||||||
plugin.extendCli(cli);
|
// @ts-expect-error: TODO annoying type incompatibility
|
||||||
|
plugin.extendCli!(cli);
|
||||||
cli.parse(['node', 'test', `docs:version:${pluginId}`, '2.0.0']);
|
cli.parse(['node', 'test', `docs:version:${pluginId}`, '2.0.0']);
|
||||||
|
expect(mock).toHaveBeenCalledTimes(1);
|
||||||
expect(mock).toHaveBeenCalledWith('2.0.0', siteDir, pluginId, {
|
expect(mock).toHaveBeenCalledWith('2.0.0', siteDir, pluginId, {
|
||||||
path: routeBasePath,
|
path: routeBasePath,
|
||||||
sidebarPath,
|
sidebarPath,
|
||||||
|
@ -509,17 +532,17 @@ describe('versioned website (community)', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getPathToWatch', () => {
|
test('getPathToWatch', () => {
|
||||||
const pathToWatch = plugin.getPathsToWatch();
|
const pathToWatch = plugin.getPathsToWatch!();
|
||||||
const matchPattern = pathToWatch.map((filepath) =>
|
const matchPattern = pathToWatch.map((filepath) =>
|
||||||
posixPath(path.relative(siteDir, filepath)),
|
posixPath(path.relative(siteDir, filepath)),
|
||||||
);
|
);
|
||||||
expect(matchPattern).not.toEqual([]);
|
expect(matchPattern).not.toEqual([]);
|
||||||
expect(matchPattern).toMatchInlineSnapshot(`
|
expect(matchPattern).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
|
"community_sidebars.json",
|
||||||
"community/**/*.{md,mdx}",
|
"community/**/*.{md,mdx}",
|
||||||
"community_versioned_sidebars/version-1.0.0-sidebars.json",
|
"community_versioned_sidebars/version-1.0.0-sidebars.json",
|
||||||
"community_versioned_docs/version-1.0.0/**/*.{md,mdx}",
|
"community_versioned_docs/version-1.0.0/**/*.{md,mdx}",
|
||||||
"community_sidebars.json",
|
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
expect(isMatch('community/team.md', matchPattern)).toEqual(true);
|
expect(isMatch('community/team.md', matchPattern)).toEqual(true);
|
||||||
|
@ -545,27 +568,29 @@ describe('versioned website (community)', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('content', async () => {
|
test('content', async () => {
|
||||||
const content = await plugin.loadContent();
|
const content = await plugin.loadContent!();
|
||||||
const {
|
expect(content.loadedVersions.length).toEqual(2);
|
||||||
docsMetadata,
|
const [currentVersion, version100] = content.loadedVersions;
|
||||||
docsSidebars,
|
|
||||||
versionToSidebars,
|
|
||||||
permalinkToSidebar,
|
|
||||||
} = content;
|
|
||||||
|
|
||||||
expect(docsMetadata.team).toEqual({
|
expect(findDocById(currentVersion, 'team')).toEqual({
|
||||||
|
...defaultDocMetadata,
|
||||||
id: 'team',
|
id: 'team',
|
||||||
unversionedId: 'team',
|
unversionedId: 'team',
|
||||||
isDocsHomePage: false,
|
isDocsHomePage: false,
|
||||||
permalink: '/community/next/team',
|
permalink: '/community/next/team',
|
||||||
slug: '/team',
|
slug: '/team',
|
||||||
source: path.join('@site', routeBasePath, 'team.md'),
|
source: path.join(
|
||||||
|
'@site',
|
||||||
|
path.relative(siteDir, currentVersion.docsDirPath),
|
||||||
|
'team.md',
|
||||||
|
),
|
||||||
title: 'team',
|
title: 'team',
|
||||||
description: 'Team current version',
|
description: 'Team current version',
|
||||||
version: 'next',
|
version: 'current',
|
||||||
sidebar: 'community',
|
sidebar: 'community',
|
||||||
});
|
});
|
||||||
expect(docsMetadata['version-1.0.0/team']).toEqual({
|
expect(findDocById(version100, 'team')).toEqual({
|
||||||
|
...defaultDocMetadata,
|
||||||
id: 'version-1.0.0/team',
|
id: 'version-1.0.0/team',
|
||||||
unversionedId: 'team',
|
unversionedId: 'team',
|
||||||
isDocsHomePage: false,
|
isDocsHomePage: false,
|
||||||
|
@ -573,8 +598,7 @@ describe('versioned website (community)', () => {
|
||||||
slug: '/team',
|
slug: '/team',
|
||||||
source: path.join(
|
source: path.join(
|
||||||
'@site',
|
'@site',
|
||||||
path.relative(siteDir, versionedDir),
|
path.relative(siteDir, version100.docsDirPath),
|
||||||
'version-1.0.0',
|
|
||||||
'team.md',
|
'team.md',
|
||||||
),
|
),
|
||||||
title: 'team',
|
title: 'team',
|
||||||
|
@ -583,38 +607,18 @@ describe('versioned website (community)', () => {
|
||||||
sidebar: 'version-1.0.0/community',
|
sidebar: 'version-1.0.0/community',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(docsSidebars).toMatchSnapshot('all sidebars');
|
expect(currentVersion.sidebars).toMatchSnapshot('current version sidebars');
|
||||||
expect(versionToSidebars).toMatchSnapshot(
|
expect(version100.sidebars).toMatchSnapshot('100 version sidebars');
|
||||||
'sidebars needed for each version',
|
|
||||||
);
|
|
||||||
|
|
||||||
const {actions, utils} = createFakeActions(pluginContentDir);
|
const {actions, utils} = createFakeActions(pluginContentDir);
|
||||||
await plugin.contentLoaded({
|
await plugin.contentLoaded!({
|
||||||
content,
|
content,
|
||||||
actions,
|
actions,
|
||||||
|
allContent: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
// The created base metadata for each nested docs route is smartly chunked/ splitted across version
|
utils.checkVersionMetadataPropCreated(currentVersion);
|
||||||
const latestVersionBaseMetadata = utils.getCreatedDataByPrefix(
|
utils.checkVersionMetadataPropCreated(version100);
|
||||||
'community-route-',
|
|
||||||
);
|
|
||||||
expect(latestVersionBaseMetadata).toMatchSnapshot(
|
|
||||||
'base metadata for latest version',
|
|
||||||
);
|
|
||||||
expect(latestVersionBaseMetadata.docsSidebars).not.toEqual(docsSidebars);
|
|
||||||
expect(latestVersionBaseMetadata.permalinkToSidebar).not.toEqual(
|
|
||||||
permalinkToSidebar,
|
|
||||||
);
|
|
||||||
const nextVersionBaseMetadata = utils.getCreatedDataByPrefix(
|
|
||||||
'community-next-route-',
|
|
||||||
);
|
|
||||||
expect(nextVersionBaseMetadata).toMatchSnapshot(
|
|
||||||
'base metadata for next version',
|
|
||||||
);
|
|
||||||
expect(nextVersionBaseMetadata.docsSidebars).not.toEqual(docsSidebars);
|
|
||||||
expect(nextVersionBaseMetadata.permalinkToSidebar).not.toEqual(
|
|
||||||
permalinkToSidebar,
|
|
||||||
);
|
|
||||||
|
|
||||||
utils.expectSnapshot();
|
utils.expectSnapshot();
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,7 +9,7 @@ import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import shell from 'shelljs';
|
import shell from 'shelljs';
|
||||||
|
|
||||||
import lastUpdate from '../lastUpdate';
|
import {getFileLastUpdate} from '../lastUpdate';
|
||||||
|
|
||||||
describe('lastUpdate', () => {
|
describe('lastUpdate', () => {
|
||||||
const existingFilePath = path.join(
|
const existingFilePath = path.join(
|
||||||
|
@ -17,7 +17,7 @@ describe('lastUpdate', () => {
|
||||||
'__fixtures__/simple-site/docs/hello.md',
|
'__fixtures__/simple-site/docs/hello.md',
|
||||||
);
|
);
|
||||||
test('existing test file in repository with Git timestamp', async () => {
|
test('existing test file in repository with Git timestamp', async () => {
|
||||||
const lastUpdateData = await lastUpdate(existingFilePath);
|
const lastUpdateData = await getFileLastUpdate(existingFilePath);
|
||||||
expect(lastUpdateData).not.toBeNull();
|
expect(lastUpdateData).not.toBeNull();
|
||||||
|
|
||||||
const {author, timestamp} = lastUpdateData;
|
const {author, timestamp} = lastUpdateData;
|
||||||
|
@ -36,29 +36,29 @@ describe('lastUpdate', () => {
|
||||||
'__fixtures__',
|
'__fixtures__',
|
||||||
'.nonExisting',
|
'.nonExisting',
|
||||||
);
|
);
|
||||||
expect(await lastUpdate(nonExistingFilePath)).toBeNull();
|
expect(await getFileLastUpdate(nonExistingFilePath)).toBeNull();
|
||||||
expect(consoleMock).toHaveBeenCalledTimes(1);
|
expect(consoleMock).toHaveBeenCalledTimes(1);
|
||||||
expect(consoleMock).toHaveBeenCalledWith(
|
expect(consoleMock).toHaveBeenCalledWith(
|
||||||
new Error(
|
new Error(
|
||||||
`Command failed with exit code 128: git log -1 --format=%ct, %an ${nonExistingFilePath}`,
|
`Command failed with exit code 128: git log -1 --format=%ct, %an ${nonExistingFilePath}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(await lastUpdate(null)).toBeNull();
|
expect(await getFileLastUpdate(null)).toBeNull();
|
||||||
expect(await lastUpdate(undefined)).toBeNull();
|
expect(await getFileLastUpdate(undefined)).toBeNull();
|
||||||
consoleMock.mockRestore();
|
consoleMock.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('temporary created file that has no git timestamp', async () => {
|
test('temporary created file that has no git timestamp', async () => {
|
||||||
const tempFilePath = path.join(__dirname, '__fixtures__', '.temp');
|
const tempFilePath = path.join(__dirname, '__fixtures__', '.temp');
|
||||||
fs.writeFileSync(tempFilePath, 'Lorem ipsum :)');
|
fs.writeFileSync(tempFilePath, 'Lorem ipsum :)');
|
||||||
expect(await lastUpdate(tempFilePath)).toBeNull();
|
expect(await getFileLastUpdate(tempFilePath)).toBeNull();
|
||||||
fs.unlinkSync(tempFilePath);
|
fs.unlinkSync(tempFilePath);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Git does not exist', async () => {
|
test('Git does not exist', async () => {
|
||||||
const mock = jest.spyOn(shell, 'which').mockImplementationOnce(() => null);
|
const mock = jest.spyOn(shell, 'which').mockImplementationOnce(() => null);
|
||||||
const consoleMock = jest.spyOn(console, 'warn').mockImplementation();
|
const consoleMock = jest.spyOn(console, 'warn').mockImplementation();
|
||||||
const lastUpdateData = await lastUpdate(existingFilePath);
|
const lastUpdateData = await getFileLastUpdate(existingFilePath);
|
||||||
expect(lastUpdateData).toBeNull();
|
expect(lastUpdateData).toBeNull();
|
||||||
expect(consoleMock).toHaveBeenLastCalledWith(
|
expect(consoleMock).toHaveBeenLastCalledWith(
|
||||||
'Sorry, the docs plugin last update options require Git.',
|
'Sorry, the docs plugin last update options require Git.',
|
||||||
|
|
|
@ -1,464 +0,0 @@
|
||||||
/**
|
|
||||||
* 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 path from 'path';
|
|
||||||
import {loadContext} from '@docusaurus/core/src/server/index';
|
|
||||||
import processMetadata from '../metadata';
|
|
||||||
import loadEnv from '../env';
|
|
||||||
import {MetadataRaw, Env, MetadataOptions} from '../types';
|
|
||||||
import {LoadContext} from '@docusaurus/types';
|
|
||||||
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
|
|
||||||
|
|
||||||
const fixtureDir = path.join(__dirname, '__fixtures__');
|
|
||||||
|
|
||||||
function createTestHelpers({
|
|
||||||
siteDir,
|
|
||||||
context,
|
|
||||||
env,
|
|
||||||
options,
|
|
||||||
}: {
|
|
||||||
siteDir: string;
|
|
||||||
context: LoadContext;
|
|
||||||
env: Env;
|
|
||||||
options: MetadataOptions;
|
|
||||||
}) {
|
|
||||||
async function testMeta(
|
|
||||||
refDir: string,
|
|
||||||
source: string,
|
|
||||||
expectedMetadata: Omit<MetadataRaw, 'source'>,
|
|
||||||
) {
|
|
||||||
const metadata = await processMetadata({
|
|
||||||
source,
|
|
||||||
refDir,
|
|
||||||
context,
|
|
||||||
options,
|
|
||||||
env,
|
|
||||||
});
|
|
||||||
expect(metadata).toEqual({
|
|
||||||
...expectedMetadata,
|
|
||||||
source: path.join('@site', path.relative(siteDir, refDir), source),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testSlug(
|
|
||||||
refDir: string,
|
|
||||||
source: string,
|
|
||||||
expectedPermalink: string,
|
|
||||||
) {
|
|
||||||
const metadata = await processMetadata({
|
|
||||||
source,
|
|
||||||
refDir,
|
|
||||||
context,
|
|
||||||
options,
|
|
||||||
env,
|
|
||||||
});
|
|
||||||
expect(metadata.permalink).toEqual(expectedPermalink);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {testMeta, testSlug};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('simple site', () => {
|
|
||||||
const siteDir = path.join(fixtureDir, 'simple-site');
|
|
||||||
const context = loadContext(siteDir);
|
|
||||||
const routeBasePath = 'docs';
|
|
||||||
const docsDir = path.resolve(siteDir, routeBasePath);
|
|
||||||
const env = loadEnv(siteDir, DEFAULT_PLUGIN_ID);
|
|
||||||
const options = {routeBasePath};
|
|
||||||
|
|
||||||
const {testMeta, testSlug} = createTestHelpers({
|
|
||||||
siteDir,
|
|
||||||
context,
|
|
||||||
options,
|
|
||||||
env,
|
|
||||||
});
|
|
||||||
|
|
||||||
test('normal docs', async () => {
|
|
||||||
await testMeta(docsDir, path.join('foo', 'bar.md'), {
|
|
||||||
id: 'foo/bar',
|
|
||||||
unversionedId: 'foo/bar',
|
|
||||||
isDocsHomePage: false,
|
|
||||||
permalink: '/docs/foo/bar',
|
|
||||||
slug: '/foo/bar',
|
|
||||||
title: 'Bar',
|
|
||||||
description: 'This is custom description',
|
|
||||||
});
|
|
||||||
await testMeta(docsDir, path.join('hello.md'), {
|
|
||||||
id: 'hello',
|
|
||||||
unversionedId: 'hello',
|
|
||||||
isDocsHomePage: false,
|
|
||||||
permalink: '/docs/hello',
|
|
||||||
slug: '/hello',
|
|
||||||
title: 'Hello, World !',
|
|
||||||
description: `Hi, Endilie here :)`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('homePageId doc', async () => {
|
|
||||||
const {testMeta: testMetaLocal} = createTestHelpers({
|
|
||||||
siteDir,
|
|
||||||
options: {
|
|
||||||
routeBasePath,
|
|
||||||
homePageId: 'hello',
|
|
||||||
},
|
|
||||||
context,
|
|
||||||
env,
|
|
||||||
});
|
|
||||||
|
|
||||||
await testMetaLocal(docsDir, path.join('hello.md'), {
|
|
||||||
id: 'hello',
|
|
||||||
unversionedId: 'hello',
|
|
||||||
isDocsHomePage: true,
|
|
||||||
permalink: '/docs/',
|
|
||||||
slug: '/',
|
|
||||||
title: 'Hello, World !',
|
|
||||||
description: `Hi, Endilie here :)`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('homePageId doc nested', async () => {
|
|
||||||
const {testMeta: testMetaLocal} = createTestHelpers({
|
|
||||||
siteDir,
|
|
||||||
options: {
|
|
||||||
routeBasePath,
|
|
||||||
homePageId: 'foo/bar',
|
|
||||||
},
|
|
||||||
context,
|
|
||||||
env,
|
|
||||||
});
|
|
||||||
|
|
||||||
await testMetaLocal(docsDir, path.join('foo', 'bar.md'), {
|
|
||||||
id: 'foo/bar',
|
|
||||||
unversionedId: 'foo/bar',
|
|
||||||
isDocsHomePage: true,
|
|
||||||
permalink: '/docs/',
|
|
||||||
slug: '/',
|
|
||||||
title: 'Bar',
|
|
||||||
description: 'This is custom description',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('docs with editUrl', async () => {
|
|
||||||
const {testMeta: testMetaLocal} = createTestHelpers({
|
|
||||||
siteDir,
|
|
||||||
options: {
|
|
||||||
routeBasePath,
|
|
||||||
editUrl: 'https://github.com/facebook/docusaurus/edit/master/website',
|
|
||||||
},
|
|
||||||
context,
|
|
||||||
env,
|
|
||||||
});
|
|
||||||
|
|
||||||
await testMetaLocal(docsDir, path.join('foo', 'baz.md'), {
|
|
||||||
id: 'foo/baz',
|
|
||||||
unversionedId: 'foo/baz',
|
|
||||||
isDocsHomePage: false,
|
|
||||||
permalink: '/docs/foo/bazSlug.html',
|
|
||||||
slug: '/foo/bazSlug.html',
|
|
||||||
title: 'baz',
|
|
||||||
editUrl:
|
|
||||||
'https://github.com/facebook/docusaurus/edit/master/website/docs/foo/baz.md',
|
|
||||||
description: 'Images',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('docs with custom editUrl & unrelated frontmatter', async () => {
|
|
||||||
await testMeta(docsDir, 'lorem.md', {
|
|
||||||
id: 'lorem',
|
|
||||||
unversionedId: 'lorem',
|
|
||||||
isDocsHomePage: false,
|
|
||||||
permalink: '/docs/lorem',
|
|
||||||
slug: '/lorem',
|
|
||||||
title: 'lorem',
|
|
||||||
editUrl: 'https://github.com/customUrl/docs/lorem.md',
|
|
||||||
description: 'Lorem ipsum.',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('docs with last update time and author', async () => {
|
|
||||||
const {testMeta: testMetaLocal} = createTestHelpers({
|
|
||||||
siteDir,
|
|
||||||
options: {
|
|
||||||
routeBasePath,
|
|
||||||
showLastUpdateAuthor: true,
|
|
||||||
showLastUpdateTime: true,
|
|
||||||
},
|
|
||||||
context,
|
|
||||||
env,
|
|
||||||
});
|
|
||||||
|
|
||||||
await testMetaLocal(docsDir, 'lorem.md', {
|
|
||||||
id: 'lorem',
|
|
||||||
unversionedId: 'lorem',
|
|
||||||
isDocsHomePage: false,
|
|
||||||
permalink: '/docs/lorem',
|
|
||||||
slug: '/lorem',
|
|
||||||
title: 'lorem',
|
|
||||||
editUrl: 'https://github.com/customUrl/docs/lorem.md',
|
|
||||||
description: 'Lorem ipsum.',
|
|
||||||
lastUpdatedAt: 1539502055,
|
|
||||||
lastUpdatedBy: 'Author',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('docs with null custom_edit_url', async () => {
|
|
||||||
const {testMeta: testMetaLocal} = createTestHelpers({
|
|
||||||
siteDir,
|
|
||||||
options: {
|
|
||||||
routeBasePath,
|
|
||||||
showLastUpdateAuthor: true,
|
|
||||||
showLastUpdateTime: true,
|
|
||||||
},
|
|
||||||
context,
|
|
||||||
env,
|
|
||||||
});
|
|
||||||
|
|
||||||
await testMetaLocal(docsDir, 'ipsum.md', {
|
|
||||||
id: 'ipsum',
|
|
||||||
unversionedId: 'ipsum',
|
|
||||||
isDocsHomePage: false,
|
|
||||||
permalink: '/docs/ipsum',
|
|
||||||
slug: '/ipsum',
|
|
||||||
title: 'ipsum',
|
|
||||||
editUrl: null,
|
|
||||||
description: 'Lorem ipsum.',
|
|
||||||
lastUpdatedAt: 1539502055,
|
|
||||||
lastUpdatedBy: 'Author',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('docs with slugs', async () => {
|
|
||||||
await testSlug(
|
|
||||||
docsDir,
|
|
||||||
path.join('rootRelativeSlug.md'),
|
|
||||||
'/docs/rootRelativeSlug',
|
|
||||||
);
|
|
||||||
await testSlug(
|
|
||||||
docsDir,
|
|
||||||
path.join('rootAbsoluteSlug.md'),
|
|
||||||
'/docs/rootAbsoluteSlug',
|
|
||||||
);
|
|
||||||
await testSlug(
|
|
||||||
docsDir,
|
|
||||||
path.join('rootResolvedSlug.md'),
|
|
||||||
'/docs/hey/rootResolvedSlug',
|
|
||||||
);
|
|
||||||
await testSlug(
|
|
||||||
docsDir,
|
|
||||||
path.join('rootTryToEscapeSlug.md'),
|
|
||||||
'/docs/rootTryToEscapeSlug',
|
|
||||||
);
|
|
||||||
|
|
||||||
await testSlug(
|
|
||||||
docsDir,
|
|
||||||
path.join('slugs', 'absoluteSlug.md'),
|
|
||||||
'/docs/absoluteSlug',
|
|
||||||
);
|
|
||||||
await testSlug(
|
|
||||||
docsDir,
|
|
||||||
path.join('slugs', 'relativeSlug.md'),
|
|
||||||
'/docs/slugs/relativeSlug',
|
|
||||||
);
|
|
||||||
await testSlug(
|
|
||||||
docsDir,
|
|
||||||
path.join('slugs', 'resolvedSlug.md'),
|
|
||||||
'/docs/slugs/hey/resolvedSlug',
|
|
||||||
);
|
|
||||||
await testSlug(
|
|
||||||
docsDir,
|
|
||||||
path.join('slugs', 'tryToEscapeSlug.md'),
|
|
||||||
'/docs/tryToEscapeSlug',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('docs with invalid id', async () => {
|
|
||||||
const badSiteDir = path.join(fixtureDir, 'bad-id-site');
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
processMetadata({
|
|
||||||
source: 'invalid-id.md',
|
|
||||||
refDir: path.join(badSiteDir, 'docs'),
|
|
||||||
context,
|
|
||||||
options: {
|
|
||||||
routeBasePath,
|
|
||||||
},
|
|
||||||
env,
|
|
||||||
}),
|
|
||||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`"Document id cannot include \\"/\\"."`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('docs with slug on doc home', async () => {
|
|
||||||
const badSiteDir = path.join(fixtureDir, 'bad-slug-on-doc-home-site');
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
processMetadata({
|
|
||||||
source: 'docWithSlug.md',
|
|
||||||
refDir: path.join(badSiteDir, 'docs'),
|
|
||||||
context,
|
|
||||||
options: {
|
|
||||||
routeBasePath,
|
|
||||||
homePageId: 'docWithSlug',
|
|
||||||
},
|
|
||||||
env,
|
|
||||||
}),
|
|
||||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`"The docs homepage (homePageId=docWithSlug) is not allowed to have a frontmatter slug=docWithSlug.html => you have to chooser either homePageId or slug, not both"`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('versioned site', () => {
|
|
||||||
const siteDir = path.join(fixtureDir, 'versioned-site');
|
|
||||||
const context = loadContext(siteDir);
|
|
||||||
const routeBasePath = 'docs';
|
|
||||||
const docsDir = path.resolve(siteDir, routeBasePath);
|
|
||||||
const env = loadEnv(siteDir, DEFAULT_PLUGIN_ID);
|
|
||||||
const {docsDir: versionedDir} = env.versioning;
|
|
||||||
const options = {routeBasePath};
|
|
||||||
|
|
||||||
const {testMeta, testSlug} = createTestHelpers({
|
|
||||||
siteDir,
|
|
||||||
context,
|
|
||||||
options,
|
|
||||||
env,
|
|
||||||
});
|
|
||||||
|
|
||||||
test('next docs', async () => {
|
|
||||||
await testMeta(docsDir, path.join('foo', 'bar.md'), {
|
|
||||||
id: 'foo/bar',
|
|
||||||
unversionedId: 'foo/bar',
|
|
||||||
isDocsHomePage: false,
|
|
||||||
permalink: '/docs/next/foo/barSlug',
|
|
||||||
slug: '/foo/barSlug',
|
|
||||||
title: 'bar',
|
|
||||||
description: 'This is next version of bar.',
|
|
||||||
version: 'next',
|
|
||||||
});
|
|
||||||
await testMeta(docsDir, path.join('hello.md'), {
|
|
||||||
id: 'hello',
|
|
||||||
unversionedId: 'hello',
|
|
||||||
isDocsHomePage: false,
|
|
||||||
permalink: '/docs/next/hello',
|
|
||||||
slug: '/hello',
|
|
||||||
title: 'hello',
|
|
||||||
description: 'Hello next !',
|
|
||||||
version: 'next',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('versioned docs', async () => {
|
|
||||||
await testMeta(versionedDir, path.join('version-1.0.0', 'foo', 'bar.md'), {
|
|
||||||
id: 'version-1.0.0/foo/bar',
|
|
||||||
unversionedId: 'foo/bar',
|
|
||||||
isDocsHomePage: false,
|
|
||||||
permalink: '/docs/1.0.0/foo/barSlug',
|
|
||||||
slug: '/foo/barSlug',
|
|
||||||
title: 'bar',
|
|
||||||
description: 'Bar 1.0.0 !',
|
|
||||||
version: '1.0.0',
|
|
||||||
});
|
|
||||||
await testMeta(versionedDir, path.join('version-1.0.0', 'hello.md'), {
|
|
||||||
id: 'version-1.0.0/hello',
|
|
||||||
unversionedId: 'hello',
|
|
||||||
isDocsHomePage: false,
|
|
||||||
permalink: '/docs/1.0.0/hello',
|
|
||||||
slug: '/hello',
|
|
||||||
title: 'hello',
|
|
||||||
description: 'Hello 1.0.0 !',
|
|
||||||
version: '1.0.0',
|
|
||||||
});
|
|
||||||
await testMeta(versionedDir, path.join('version-1.0.1', 'foo', 'bar.md'), {
|
|
||||||
id: 'version-1.0.1/foo/bar',
|
|
||||||
unversionedId: 'foo/bar',
|
|
||||||
isDocsHomePage: false,
|
|
||||||
permalink: '/docs/foo/bar',
|
|
||||||
slug: '/foo/bar',
|
|
||||||
title: 'bar',
|
|
||||||
description: 'Bar 1.0.1 !',
|
|
||||||
version: '1.0.1',
|
|
||||||
});
|
|
||||||
await testMeta(versionedDir, path.join('version-1.0.1', 'hello.md'), {
|
|
||||||
id: 'version-1.0.1/hello',
|
|
||||||
unversionedId: 'hello',
|
|
||||||
isDocsHomePage: false,
|
|
||||||
permalink: '/docs/hello',
|
|
||||||
slug: '/hello',
|
|
||||||
title: 'hello',
|
|
||||||
description: 'Hello 1.0.1 !',
|
|
||||||
version: '1.0.1',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('next doc slugs', async () => {
|
|
||||||
await testSlug(
|
|
||||||
docsDir,
|
|
||||||
path.join('slugs', 'absoluteSlug.md'),
|
|
||||||
'/docs/next/absoluteSlug',
|
|
||||||
);
|
|
||||||
await testSlug(
|
|
||||||
docsDir,
|
|
||||||
path.join('slugs', 'relativeSlug.md'),
|
|
||||||
'/docs/next/slugs/relativeSlug',
|
|
||||||
);
|
|
||||||
await testSlug(
|
|
||||||
docsDir,
|
|
||||||
path.join('slugs', 'resolvedSlug.md'),
|
|
||||||
'/docs/next/slugs/hey/resolvedSlug',
|
|
||||||
);
|
|
||||||
await testSlug(
|
|
||||||
docsDir,
|
|
||||||
path.join('slugs', 'tryToEscapeSlug.md'),
|
|
||||||
'/docs/next/tryToEscapeSlug',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('versioned doc slugs', async () => {
|
|
||||||
await testSlug(
|
|
||||||
versionedDir,
|
|
||||||
path.join('version-withSlugs', 'rootAbsoluteSlug.md'),
|
|
||||||
'/docs/withSlugs/rootAbsoluteSlug',
|
|
||||||
);
|
|
||||||
await testSlug(
|
|
||||||
versionedDir,
|
|
||||||
path.join('version-withSlugs', 'rootRelativeSlug.md'),
|
|
||||||
'/docs/withSlugs/rootRelativeSlug',
|
|
||||||
);
|
|
||||||
await testSlug(
|
|
||||||
versionedDir,
|
|
||||||
path.join('version-withSlugs', 'rootResolvedSlug.md'),
|
|
||||||
'/docs/withSlugs/hey/rootResolvedSlug',
|
|
||||||
);
|
|
||||||
await testSlug(
|
|
||||||
versionedDir,
|
|
||||||
path.join('version-withSlugs', 'rootTryToEscapeSlug.md'),
|
|
||||||
'/docs/withSlugs/rootTryToEscapeSlug',
|
|
||||||
);
|
|
||||||
|
|
||||||
await testSlug(
|
|
||||||
versionedDir,
|
|
||||||
path.join('version-withSlugs', 'slugs', 'absoluteSlug.md'),
|
|
||||||
'/docs/withSlugs/absoluteSlug',
|
|
||||||
);
|
|
||||||
await testSlug(
|
|
||||||
versionedDir,
|
|
||||||
path.join('version-withSlugs', 'slugs', 'relativeSlug.md'),
|
|
||||||
'/docs/withSlugs/slugs/relativeSlug',
|
|
||||||
);
|
|
||||||
await testSlug(
|
|
||||||
versionedDir,
|
|
||||||
path.join('version-withSlugs', 'slugs', 'resolvedSlug.md'),
|
|
||||||
'/docs/withSlugs/slugs/hey/resolvedSlug',
|
|
||||||
);
|
|
||||||
await testSlug(
|
|
||||||
versionedDir,
|
|
||||||
path.join('version-withSlugs', 'slugs', 'tryToEscapeSlug.md'),
|
|
||||||
'/docs/withSlugs/tryToEscapeSlug',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -5,7 +5,7 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {PluginOptionSchema, DEFAULT_OPTIONS} from '../pluginOptionSchema';
|
import {OptionsSchema, DEFAULT_OPTIONS} from '../options';
|
||||||
import {normalizePluginOptions} from '@docusaurus/utils-validation';
|
import {normalizePluginOptions} from '@docusaurus/utils-validation';
|
||||||
|
|
||||||
// the type of remark/rehype plugins is function
|
// the type of remark/rehype plugins is function
|
||||||
|
@ -14,7 +14,7 @@ const markdownPluginsObjectStub = {};
|
||||||
|
|
||||||
describe('normalizeDocsPluginOptions', () => {
|
describe('normalizeDocsPluginOptions', () => {
|
||||||
test('should return default options for undefined user options', async () => {
|
test('should return default options for undefined user options', async () => {
|
||||||
const {value, error} = await PluginOptionSchema.validate({});
|
const {value, error} = await OptionsSchema.validate({});
|
||||||
expect(value).toEqual(DEFAULT_OPTIONS);
|
expect(value).toEqual(DEFAULT_OPTIONS);
|
||||||
expect(error).toBe(undefined);
|
expect(error).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
@ -34,9 +34,10 @@ describe('normalizeDocsPluginOptions', () => {
|
||||||
showLastUpdateAuthor: true,
|
showLastUpdateAuthor: true,
|
||||||
admonitions: {},
|
admonitions: {},
|
||||||
excludeNextVersionDocs: true,
|
excludeNextVersionDocs: true,
|
||||||
|
includeCurrentVersion: false,
|
||||||
disableVersioning: true,
|
disableVersioning: true,
|
||||||
};
|
};
|
||||||
const {value, error} = await PluginOptionSchema.validate(userOptions);
|
const {value, error} = await OptionsSchema.validate(userOptions);
|
||||||
expect(value).toEqual(userOptions);
|
expect(value).toEqual(userOptions);
|
||||||
expect(error).toBe(undefined);
|
expect(error).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
@ -50,14 +51,14 @@ describe('normalizeDocsPluginOptions', () => {
|
||||||
[markdownPluginsFunctionStub, {option1: '42'}],
|
[markdownPluginsFunctionStub, {option1: '42'}],
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const {value, error} = await PluginOptionSchema.validate(userOptions);
|
const {value, error} = await OptionsSchema.validate(userOptions);
|
||||||
expect(value).toEqual(userOptions);
|
expect(value).toEqual(userOptions);
|
||||||
expect(error).toBe(undefined);
|
expect(error).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should reject invalid remark plugin options', () => {
|
test('should reject invalid remark plugin options', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
normalizePluginOptions(PluginOptionSchema, {
|
normalizePluginOptions(OptionsSchema, {
|
||||||
remarkPlugins: [[{option1: '42'}, markdownPluginsFunctionStub]],
|
remarkPlugins: [[{option1: '42'}, markdownPluginsFunctionStub]],
|
||||||
});
|
});
|
||||||
}).toThrowErrorMatchingInlineSnapshot(
|
}).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
@ -67,7 +68,7 @@ describe('normalizeDocsPluginOptions', () => {
|
||||||
|
|
||||||
test('should reject invalid rehype plugin options', () => {
|
test('should reject invalid rehype plugin options', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
normalizePluginOptions(PluginOptionSchema, {
|
normalizePluginOptions(OptionsSchema, {
|
||||||
rehypePlugins: [
|
rehypePlugins: [
|
||||||
[
|
[
|
||||||
markdownPluginsFunctionStub,
|
markdownPluginsFunctionStub,
|
||||||
|
@ -83,7 +84,7 @@ describe('normalizeDocsPluginOptions', () => {
|
||||||
|
|
||||||
test('should reject bad path inputs', () => {
|
test('should reject bad path inputs', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
normalizePluginOptions(PluginOptionSchema, {
|
normalizePluginOptions(OptionsSchema, {
|
||||||
path: 2,
|
path: 2,
|
||||||
});
|
});
|
||||||
}).toThrowErrorMatchingInlineSnapshot(`"\\"path\\" must be a string"`);
|
}).toThrowErrorMatchingInlineSnapshot(`"\\"path\\" must be a string"`);
|
||||||
|
@ -91,7 +92,7 @@ describe('normalizeDocsPluginOptions', () => {
|
||||||
|
|
||||||
test('should reject bad include inputs', () => {
|
test('should reject bad include inputs', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
normalizePluginOptions(PluginOptionSchema, {
|
normalizePluginOptions(OptionsSchema, {
|
||||||
include: '**/*.{md,mdx}',
|
include: '**/*.{md,mdx}',
|
||||||
});
|
});
|
||||||
}).toThrowErrorMatchingInlineSnapshot(`"\\"include\\" must be an array"`);
|
}).toThrowErrorMatchingInlineSnapshot(`"\\"include\\" must be an array"`);
|
||||||
|
@ -99,7 +100,7 @@ describe('normalizeDocsPluginOptions', () => {
|
||||||
|
|
||||||
test('should reject bad showLastUpdateTime inputs', () => {
|
test('should reject bad showLastUpdateTime inputs', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
normalizePluginOptions(PluginOptionSchema, {
|
normalizePluginOptions(OptionsSchema, {
|
||||||
showLastUpdateTime: 'true',
|
showLastUpdateTime: 'true',
|
||||||
});
|
});
|
||||||
}).toThrowErrorMatchingInlineSnapshot(
|
}).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
@ -109,7 +110,7 @@ describe('normalizeDocsPluginOptions', () => {
|
||||||
|
|
||||||
test('should reject bad remarkPlugins input', () => {
|
test('should reject bad remarkPlugins input', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
normalizePluginOptions(PluginOptionSchema, {
|
normalizePluginOptions(OptionsSchema, {
|
||||||
remarkPlugins: 'remark-math',
|
remarkPlugins: 'remark-math',
|
||||||
});
|
});
|
||||||
}).toThrowErrorMatchingInlineSnapshot(
|
}).toThrowErrorMatchingInlineSnapshot(
|
|
@ -1,240 +0,0 @@
|
||||||
/**
|
|
||||||
* 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 createOrder from '../order';
|
|
||||||
|
|
||||||
describe('createOrder', () => {
|
|
||||||
test('multiple sidebars with subcategory', () => {
|
|
||||||
const result = createOrder({
|
|
||||||
docs: [
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
label: 'Category1',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
label: 'Subcategory 1',
|
|
||||||
items: [{type: 'doc', id: 'doc1'}],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
label: 'Subcategory 2',
|
|
||||||
items: [{type: 'doc', id: 'doc2'}],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
label: 'Category2',
|
|
||||||
items: [
|
|
||||||
{type: 'doc', id: 'doc3'},
|
|
||||||
{type: 'doc', id: 'doc4'},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
otherDocs: [
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
label: 'Category1',
|
|
||||||
items: [{type: 'doc', id: 'doc5'}],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
expect(result).toEqual({
|
|
||||||
doc1: {
|
|
||||||
next: 'doc2',
|
|
||||||
previous: undefined,
|
|
||||||
sidebar: 'docs',
|
|
||||||
},
|
|
||||||
doc2: {
|
|
||||||
next: 'doc3',
|
|
||||||
previous: 'doc1',
|
|
||||||
sidebar: 'docs',
|
|
||||||
},
|
|
||||||
doc3: {
|
|
||||||
next: 'doc4',
|
|
||||||
previous: 'doc2',
|
|
||||||
sidebar: 'docs',
|
|
||||||
},
|
|
||||||
doc4: {
|
|
||||||
next: undefined,
|
|
||||||
previous: 'doc3',
|
|
||||||
sidebar: 'docs',
|
|
||||||
},
|
|
||||||
doc5: {
|
|
||||||
next: undefined,
|
|
||||||
previous: undefined,
|
|
||||||
sidebar: 'otherDocs',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test('multiple sidebars without subcategory', () => {
|
|
||||||
const result = createOrder({
|
|
||||||
docs: [
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
label: 'Category1',
|
|
||||||
items: [
|
|
||||||
{type: 'doc', id: 'doc1'},
|
|
||||||
{type: 'doc', id: 'doc2'},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
label: 'Category2',
|
|
||||||
items: [
|
|
||||||
{type: 'doc', id: 'doc3'},
|
|
||||||
{type: 'doc', id: 'doc4'},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
otherDocs: [
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
label: 'Category1',
|
|
||||||
items: [{type: 'doc', id: 'doc5'}],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
expect(result).toEqual({
|
|
||||||
doc1: {
|
|
||||||
next: 'doc2',
|
|
||||||
previous: undefined,
|
|
||||||
sidebar: 'docs',
|
|
||||||
},
|
|
||||||
doc2: {
|
|
||||||
next: 'doc3',
|
|
||||||
previous: 'doc1',
|
|
||||||
sidebar: 'docs',
|
|
||||||
},
|
|
||||||
doc3: {
|
|
||||||
next: 'doc4',
|
|
||||||
previous: 'doc2',
|
|
||||||
sidebar: 'docs',
|
|
||||||
},
|
|
||||||
doc4: {
|
|
||||||
next: undefined,
|
|
||||||
previous: 'doc3',
|
|
||||||
sidebar: 'docs',
|
|
||||||
},
|
|
||||||
doc5: {
|
|
||||||
next: undefined,
|
|
||||||
previous: undefined,
|
|
||||||
sidebar: 'otherDocs',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('versioned sidebars', () => {
|
|
||||||
const result = createOrder({
|
|
||||||
docs: [
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
label: 'Category1',
|
|
||||||
items: [{type: 'doc', id: 'doc1'}],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'version-1.2.3-docs': [
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
label: 'Category1',
|
|
||||||
items: [{type: 'doc', id: 'version-1.2.3-doc2'}],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
label: 'Category2',
|
|
||||||
items: [{type: 'doc', id: 'version-1.2.3-doc1'}],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
expect(result).toEqual({
|
|
||||||
doc1: {
|
|
||||||
next: undefined,
|
|
||||||
previous: undefined,
|
|
||||||
sidebar: 'docs',
|
|
||||||
},
|
|
||||||
'version-1.2.3-doc1': {
|
|
||||||
next: undefined,
|
|
||||||
previous: 'version-1.2.3-doc2',
|
|
||||||
sidebar: 'version-1.2.3-docs',
|
|
||||||
},
|
|
||||||
'version-1.2.3-doc2': {
|
|
||||||
next: 'version-1.2.3-doc1',
|
|
||||||
previous: undefined,
|
|
||||||
sidebar: 'version-1.2.3-docs',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('multiple sidebars with subcategories, refs and external links', () => {
|
|
||||||
const result = createOrder({
|
|
||||||
docs: [
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
label: 'Category1',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
label: 'Subcategory 1',
|
|
||||||
items: [{type: 'link', href: '//example.com', label: 'bar'}],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
label: 'Subcategory 2',
|
|
||||||
items: [{type: 'doc', id: 'doc2'}],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
label: 'Subcategory 1',
|
|
||||||
items: [{type: 'link', href: '//example2.com', label: 'baz'}],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
label: 'Category2',
|
|
||||||
items: [
|
|
||||||
{type: 'doc', id: 'doc3'},
|
|
||||||
{type: 'ref', id: 'doc4'},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
otherDocs: [
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
label: 'Category1',
|
|
||||||
items: [{type: 'doc', id: 'doc5'}],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
expect(result).toEqual({
|
|
||||||
doc2: {
|
|
||||||
next: 'doc3',
|
|
||||||
previous: undefined,
|
|
||||||
sidebar: 'docs',
|
|
||||||
},
|
|
||||||
doc3: {
|
|
||||||
next: undefined,
|
|
||||||
previous: 'doc2',
|
|
||||||
sidebar: 'docs',
|
|
||||||
},
|
|
||||||
doc5: {
|
|
||||||
next: undefined,
|
|
||||||
previous: undefined,
|
|
||||||
sidebar: 'otherDocs',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('edge cases', () => {
|
|
||||||
expect(createOrder({})).toEqual({});
|
|
||||||
expect(createOrder(undefined)).toEqual({});
|
|
||||||
expect(() => createOrder(null)).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`"Cannot convert undefined or null to object"`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -6,7 +6,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import loadSidebars from '../sidebars';
|
import {
|
||||||
|
loadSidebars,
|
||||||
|
collectSidebarDocItems,
|
||||||
|
collectSidebarsDocIds,
|
||||||
|
createSidebarsUtils,
|
||||||
|
} from '../sidebars';
|
||||||
|
import {Sidebar, Sidebars} from '../types';
|
||||||
|
|
||||||
/* eslint-disable global-require, import/no-dynamic-require */
|
/* eslint-disable global-require, import/no-dynamic-require */
|
||||||
|
|
||||||
|
@ -14,13 +20,13 @@ describe('loadSidebars', () => {
|
||||||
const fixtureDir = path.join(__dirname, '__fixtures__', 'sidebars');
|
const fixtureDir = path.join(__dirname, '__fixtures__', 'sidebars');
|
||||||
test('sidebars with known sidebar item type', async () => {
|
test('sidebars with known sidebar item type', async () => {
|
||||||
const sidebarPath = path.join(fixtureDir, 'sidebars.json');
|
const sidebarPath = path.join(fixtureDir, 'sidebars.json');
|
||||||
const result = loadSidebars([sidebarPath]);
|
const result = loadSidebars(sidebarPath);
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sidebars with deep level of category', async () => {
|
test('sidebars with deep level of category', async () => {
|
||||||
const sidebarPath = path.join(fixtureDir, 'sidebars-category.js');
|
const sidebarPath = path.join(fixtureDir, 'sidebars-category.js');
|
||||||
const result = loadSidebars([sidebarPath]);
|
const result = loadSidebars(sidebarPath);
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -30,8 +36,8 @@ describe('loadSidebars', () => {
|
||||||
fixtureDir,
|
fixtureDir,
|
||||||
'sidebars-category-shorthand.js',
|
'sidebars-category-shorthand.js',
|
||||||
);
|
);
|
||||||
const sidebar1 = loadSidebars([sidebarPath1]);
|
const sidebar1 = loadSidebars(sidebarPath1);
|
||||||
const sidebar2 = loadSidebars([sidebarPath2]);
|
const sidebar2 = loadSidebars(sidebarPath2);
|
||||||
expect(sidebar1).toEqual(sidebar2);
|
expect(sidebar1).toEqual(sidebar2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -40,9 +46,7 @@ describe('loadSidebars', () => {
|
||||||
fixtureDir,
|
fixtureDir,
|
||||||
'sidebars-category-wrong-items.json',
|
'sidebars-category-wrong-items.json',
|
||||||
);
|
);
|
||||||
expect(() =>
|
expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot(
|
||||||
loadSidebars([sidebarPath]),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`"Error loading {\\"type\\":\\"category\\",\\"label\\":\\"Category Label\\",\\"items\\":\\"doc1\\"}. \\"items\\" must be an array."`,
|
`"Error loading {\\"type\\":\\"category\\",\\"label\\":\\"Category Label\\",\\"items\\":\\"doc1\\"}. \\"items\\" must be an array."`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -52,9 +56,7 @@ describe('loadSidebars', () => {
|
||||||
fixtureDir,
|
fixtureDir,
|
||||||
'sidebars-category-wrong-label.json',
|
'sidebars-category-wrong-label.json',
|
||||||
);
|
);
|
||||||
expect(() =>
|
expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot(
|
||||||
loadSidebars([sidebarPath]),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`"Error loading {\\"type\\":\\"category\\",\\"label\\":true,\\"items\\":[\\"doc1\\"]}. \\"label\\" must be a string."`,
|
`"Error loading {\\"type\\":\\"category\\",\\"label\\":true,\\"items\\":[\\"doc1\\"]}. \\"label\\" must be a string."`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -64,9 +66,7 @@ describe('loadSidebars', () => {
|
||||||
fixtureDir,
|
fixtureDir,
|
||||||
'sidebars-doc-id-not-string.json',
|
'sidebars-doc-id-not-string.json',
|
||||||
);
|
);
|
||||||
expect(() =>
|
expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot(
|
||||||
loadSidebars([sidebarPath]),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`"Error loading {\\"type\\":\\"doc\\",\\"id\\":[\\"doc1\\"]}. \\"id\\" must be a string."`,
|
`"Error loading {\\"type\\":\\"doc\\",\\"id\\":[\\"doc1\\"]}. \\"id\\" must be a string."`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -76,60 +76,75 @@ describe('loadSidebars', () => {
|
||||||
fixtureDir,
|
fixtureDir,
|
||||||
'sidebars-first-level-not-category.js',
|
'sidebars-first-level-not-category.js',
|
||||||
);
|
);
|
||||||
const result = loadSidebars([sidebarPath]);
|
const result = loadSidebars(sidebarPath);
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sidebars link', async () => {
|
test('sidebars link', async () => {
|
||||||
const sidebarPath = path.join(fixtureDir, 'sidebars-link.json');
|
const sidebarPath = path.join(fixtureDir, 'sidebars-link.json');
|
||||||
const result = loadSidebars([sidebarPath]);
|
const result = loadSidebars(sidebarPath);
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sidebars link wrong label', async () => {
|
test('sidebars link wrong label', async () => {
|
||||||
const sidebarPath = path.join(fixtureDir, 'sidebars-link-wrong-label.json');
|
const sidebarPath = path.join(fixtureDir, 'sidebars-link-wrong-label.json');
|
||||||
expect(() =>
|
expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot(
|
||||||
loadSidebars([sidebarPath]),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`"Error loading {\\"type\\":\\"link\\",\\"label\\":false,\\"href\\":\\"https://github.com\\"}. \\"label\\" must be a string."`,
|
`"Error loading {\\"type\\":\\"link\\",\\"label\\":false,\\"href\\":\\"https://github.com\\"}. \\"label\\" must be a string."`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sidebars link wrong href', async () => {
|
test('sidebars link wrong href', async () => {
|
||||||
const sidebarPath = path.join(fixtureDir, 'sidebars-link-wrong-href.json');
|
const sidebarPath = path.join(fixtureDir, 'sidebars-link-wrong-href.json');
|
||||||
expect(() =>
|
expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot(
|
||||||
loadSidebars([sidebarPath]),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`"Error loading {\\"type\\":\\"link\\",\\"label\\":\\"GitHub\\",\\"href\\":[\\"example.com\\"]}. \\"href\\" must be a string."`,
|
`"Error loading {\\"type\\":\\"link\\",\\"label\\":\\"GitHub\\",\\"href\\":[\\"example.com\\"]}. \\"href\\" must be a string."`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sidebars with unknown sidebar item type', async () => {
|
test('sidebars with unknown sidebar item type', async () => {
|
||||||
const sidebarPath = path.join(fixtureDir, 'sidebars-unknown-type.json');
|
const sidebarPath = path.join(fixtureDir, 'sidebars-unknown-type.json');
|
||||||
expect(() =>
|
expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot(
|
||||||
loadSidebars([sidebarPath]),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`"Unknown sidebar item type [superman]. Sidebar item={\\"type\\":\\"superman\\"} "`,
|
`"Unknown sidebar item type [superman]. Sidebar item={\\"type\\":\\"superman\\"} "`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sidebars with known sidebar item type but wrong field', async () => {
|
test('sidebars with known sidebar item type but wrong field', async () => {
|
||||||
const sidebarPath = path.join(fixtureDir, 'sidebars-wrong-field.json');
|
const sidebarPath = path.join(fixtureDir, 'sidebars-wrong-field.json');
|
||||||
expect(() =>
|
expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot(
|
||||||
loadSidebars([sidebarPath]),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`"Unknown sidebar item keys: href. Item: {\\"type\\":\\"category\\",\\"label\\":\\"category\\",\\"href\\":\\"https://github.com\\"}"`,
|
`"Unknown sidebar item keys: href. Item: {\\"type\\":\\"category\\",\\"label\\":\\"category\\",\\"href\\":\\"https://github.com\\"}"`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('no sidebars', () => {
|
test('unexisting path', () => {
|
||||||
const result = loadSidebars(null);
|
expect(() => loadSidebars('badpath')).toThrowErrorMatchingInlineSnapshot(
|
||||||
expect(result).toEqual({});
|
`"No sidebar file exist at path: badpath"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('undefined path', () => {
|
||||||
|
expect(() =>
|
||||||
|
loadSidebars(
|
||||||
|
// @ts-expect-error: bad arg
|
||||||
|
undefined,
|
||||||
|
),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"sidebarFilePath not provided: undefined"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('null path', () => {
|
||||||
|
expect(() =>
|
||||||
|
loadSidebars(
|
||||||
|
// @ts-expect-error: bad arg
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"sidebarFilePath not provided: null"`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sidebars with category.collapsed property', async () => {
|
test('sidebars with category.collapsed property', async () => {
|
||||||
const sidebarPath = path.join(fixtureDir, 'sidebars-collapsed.json');
|
const sidebarPath = path.join(fixtureDir, 'sidebars-collapsed.json');
|
||||||
const result = loadSidebars([sidebarPath]);
|
const result = loadSidebars(sidebarPath);
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -138,7 +153,177 @@ describe('loadSidebars', () => {
|
||||||
fixtureDir,
|
fixtureDir,
|
||||||
'sidebars-collapsed-first-level.json',
|
'sidebars-collapsed-first-level.json',
|
||||||
);
|
);
|
||||||
const result = loadSidebars([sidebarPath]);
|
const result = loadSidebars(sidebarPath);
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('collectSidebarDocItems', () => {
|
||||||
|
test('can collect recursively', async () => {
|
||||||
|
const sidebar: Sidebar = [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
collapsed: false,
|
||||||
|
label: 'Category1',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
collapsed: false,
|
||||||
|
label: 'Subcategory 1',
|
||||||
|
items: [{type: 'doc', id: 'doc1'}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
collapsed: false,
|
||||||
|
label: 'Subcategory 2',
|
||||||
|
items: [
|
||||||
|
{type: 'doc', id: 'doc2'},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
collapsed: false,
|
||||||
|
label: 'Sub sub category 1',
|
||||||
|
items: [{type: 'doc', id: 'doc3'}],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
collapsed: false,
|
||||||
|
label: 'Category2',
|
||||||
|
items: [
|
||||||
|
{type: 'doc', id: 'doc4'},
|
||||||
|
{type: 'doc', id: 'doc5'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(collectSidebarDocItems(sidebar).map((doc) => doc.id)).toEqual([
|
||||||
|
'doc1',
|
||||||
|
'doc2',
|
||||||
|
'doc3',
|
||||||
|
'doc4',
|
||||||
|
'doc5',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('collectSidebarsDocItems', () => {
|
||||||
|
test('can collect sidebars doc items', async () => {
|
||||||
|
const sidebar1: Sidebar = [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
collapsed: false,
|
||||||
|
label: 'Category1',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
collapsed: false,
|
||||||
|
label: 'Subcategory 1',
|
||||||
|
items: [{type: 'doc', id: 'doc1'}],
|
||||||
|
},
|
||||||
|
{type: 'doc', id: 'doc2'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const sidebar2: Sidebar = [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
collapsed: false,
|
||||||
|
label: 'Category2',
|
||||||
|
items: [
|
||||||
|
{type: 'doc', id: 'doc3'},
|
||||||
|
{type: 'doc', id: 'doc4'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const sidebar3: Sidebar = [
|
||||||
|
{type: 'doc', id: 'doc5'},
|
||||||
|
{type: 'doc', id: 'doc6'},
|
||||||
|
];
|
||||||
|
expect(collectSidebarsDocIds({sidebar1, sidebar2, sidebar3})).toEqual({
|
||||||
|
sidebar1: ['doc1', 'doc2'],
|
||||||
|
sidebar2: ['doc3', 'doc4'],
|
||||||
|
sidebar3: ['doc5', 'doc6'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createSidebarsUtils', () => {
|
||||||
|
const sidebar1: Sidebar = [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
collapsed: false,
|
||||||
|
label: 'Category1',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
collapsed: false,
|
||||||
|
label: 'Subcategory 1',
|
||||||
|
items: [{type: 'doc', id: 'doc1'}],
|
||||||
|
},
|
||||||
|
{type: 'doc', id: 'doc2'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const sidebar2: Sidebar = [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
collapsed: false,
|
||||||
|
label: 'Category2',
|
||||||
|
items: [
|
||||||
|
{type: 'doc', id: 'doc3'},
|
||||||
|
{type: 'doc', id: 'doc4'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const sidebars: Sidebars = {sidebar1, sidebar2};
|
||||||
|
|
||||||
|
const {
|
||||||
|
getFirstDocIdOfFirstSidebar,
|
||||||
|
getSidebarNameByDocId,
|
||||||
|
getDocNavigation,
|
||||||
|
} = createSidebarsUtils(sidebars);
|
||||||
|
|
||||||
|
test('getSidebarNameByDocId', async () => {
|
||||||
|
expect(getFirstDocIdOfFirstSidebar()).toEqual('doc1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getSidebarNameByDocId', async () => {
|
||||||
|
expect(getSidebarNameByDocId('doc1')).toEqual('sidebar1');
|
||||||
|
expect(getSidebarNameByDocId('doc2')).toEqual('sidebar1');
|
||||||
|
expect(getSidebarNameByDocId('doc3')).toEqual('sidebar2');
|
||||||
|
expect(getSidebarNameByDocId('doc4')).toEqual('sidebar2');
|
||||||
|
expect(getSidebarNameByDocId('doc5')).toEqual(undefined);
|
||||||
|
expect(getSidebarNameByDocId('doc6')).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getDocNavigation', async () => {
|
||||||
|
expect(getDocNavigation('doc1')).toEqual({
|
||||||
|
sidebarName: 'sidebar1',
|
||||||
|
previousId: undefined,
|
||||||
|
nextId: 'doc2',
|
||||||
|
});
|
||||||
|
expect(getDocNavigation('doc2')).toEqual({
|
||||||
|
sidebarName: 'sidebar1',
|
||||||
|
previousId: 'doc1',
|
||||||
|
nextId: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getDocNavigation('doc3')).toEqual({
|
||||||
|
sidebarName: 'sidebar2',
|
||||||
|
previousId: undefined,
|
||||||
|
nextId: 'doc4',
|
||||||
|
});
|
||||||
|
expect(getDocNavigation('doc4')).toEqual({
|
||||||
|
sidebarName: 'sidebar2',
|
||||||
|
previousId: 'doc3',
|
||||||
|
nextId: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,345 @@
|
||||||
|
/**
|
||||||
|
* 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 path from 'path';
|
||||||
|
import {
|
||||||
|
getVersionsFilePath,
|
||||||
|
getVersionedDocsDirPath,
|
||||||
|
getVersionedSidebarsDirPath,
|
||||||
|
readVersionsMetadata,
|
||||||
|
} from '../versions';
|
||||||
|
import {DEFAULT_OPTIONS} from '../options';
|
||||||
|
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
|
||||||
|
import {VersionMetadata} from '../types';
|
||||||
|
|
||||||
|
describe('version paths', () => {
|
||||||
|
test('getVersionedDocsDirPath', () => {
|
||||||
|
expect(getVersionsFilePath('someSiteDir', DEFAULT_PLUGIN_ID)).toBe(
|
||||||
|
'someSiteDir/versions.json',
|
||||||
|
);
|
||||||
|
expect(getVersionsFilePath('otherSite/dir', 'pluginId')).toBe(
|
||||||
|
'otherSite/dir/pluginId_versions.json',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getVersionedDocsDirPath', () => {
|
||||||
|
expect(getVersionedDocsDirPath('someSiteDir', DEFAULT_PLUGIN_ID)).toBe(
|
||||||
|
'someSiteDir/versioned_docs',
|
||||||
|
);
|
||||||
|
expect(getVersionedDocsDirPath('otherSite/dir', 'pluginId')).toBe(
|
||||||
|
'otherSite/dir/pluginId_versioned_docs',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getVersionedSidebarsDirPath', () => {
|
||||||
|
expect(getVersionedSidebarsDirPath('someSiteDir', DEFAULT_PLUGIN_ID)).toBe(
|
||||||
|
'someSiteDir/versioned_sidebars',
|
||||||
|
);
|
||||||
|
expect(getVersionedSidebarsDirPath('otherSite/dir', 'pluginId')).toBe(
|
||||||
|
'otherSite/dir/pluginId_versioned_sidebars',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('simple site', () => {
|
||||||
|
const simpleSiteDir = path.resolve(
|
||||||
|
path.join(__dirname, '__fixtures__', 'simple-site'),
|
||||||
|
);
|
||||||
|
const defaultOptions = {
|
||||||
|
id: DEFAULT_PLUGIN_ID,
|
||||||
|
...DEFAULT_OPTIONS,
|
||||||
|
};
|
||||||
|
const defaultContext = {
|
||||||
|
siteDir: simpleSiteDir,
|
||||||
|
baseUrl: '/',
|
||||||
|
};
|
||||||
|
|
||||||
|
const vCurrent: VersionMetadata = {
|
||||||
|
docsDirPath: path.join(simpleSiteDir, 'docs'),
|
||||||
|
isLast: true,
|
||||||
|
routePriority: -1,
|
||||||
|
sidebarFilePath: path.join(simpleSiteDir, 'sidebars.json'),
|
||||||
|
versionLabel: 'Next',
|
||||||
|
versionName: 'current',
|
||||||
|
versionPath: '/docs',
|
||||||
|
};
|
||||||
|
|
||||||
|
test('readVersionsMetadata simple site', () => {
|
||||||
|
const versionsMetadata = readVersionsMetadata({
|
||||||
|
options: defaultOptions,
|
||||||
|
context: defaultContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(versionsMetadata).toEqual([vCurrent]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('readVersionsMetadata simple site with base url', () => {
|
||||||
|
const versionsMetadata = readVersionsMetadata({
|
||||||
|
options: defaultOptions,
|
||||||
|
context: {
|
||||||
|
...defaultContext,
|
||||||
|
baseUrl: '/myBaseUrl',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(versionsMetadata).toEqual([
|
||||||
|
{
|
||||||
|
...vCurrent,
|
||||||
|
versionPath: '/myBaseUrl/docs',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('readVersionsMetadata simple site with base url', () => {
|
||||||
|
expect(() =>
|
||||||
|
readVersionsMetadata({
|
||||||
|
options: {...defaultOptions, disableVersioning: true},
|
||||||
|
context: defaultContext,
|
||||||
|
}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Docs: using disableVersioning=true option on a non-versioned site does not make sense"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('readVersionsMetadata simple site with base url', () => {
|
||||||
|
expect(() =>
|
||||||
|
readVersionsMetadata({
|
||||||
|
options: {...defaultOptions, includeCurrentVersion: false},
|
||||||
|
context: defaultContext,
|
||||||
|
}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"It is not possible to use docs without any version. Please check the configuration of these options: includeCurrentVersion=false disableVersioning=false"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('versioned site, pluginId=default', () => {
|
||||||
|
const versionedSiteDir = path.resolve(
|
||||||
|
path.join(__dirname, '__fixtures__', 'versioned-site'),
|
||||||
|
);
|
||||||
|
const defaultOptions = {
|
||||||
|
id: DEFAULT_PLUGIN_ID,
|
||||||
|
...DEFAULT_OPTIONS,
|
||||||
|
};
|
||||||
|
const defaultContext = {
|
||||||
|
siteDir: versionedSiteDir,
|
||||||
|
baseUrl: '/',
|
||||||
|
};
|
||||||
|
|
||||||
|
const vCurrent: VersionMetadata = {
|
||||||
|
docsDirPath: path.join(versionedSiteDir, 'docs'),
|
||||||
|
isLast: false,
|
||||||
|
routePriority: undefined,
|
||||||
|
sidebarFilePath: path.join(versionedSiteDir, 'sidebars.json'),
|
||||||
|
versionLabel: 'Next',
|
||||||
|
versionName: 'current',
|
||||||
|
versionPath: '/docs/next',
|
||||||
|
};
|
||||||
|
|
||||||
|
const v101: VersionMetadata = {
|
||||||
|
docsDirPath: path.join(versionedSiteDir, 'versioned_docs/version-1.0.1'),
|
||||||
|
isLast: true,
|
||||||
|
routePriority: -1,
|
||||||
|
sidebarFilePath: path.join(
|
||||||
|
versionedSiteDir,
|
||||||
|
'versioned_sidebars/version-1.0.1-sidebars.json',
|
||||||
|
),
|
||||||
|
versionLabel: '1.0.1',
|
||||||
|
versionName: '1.0.1',
|
||||||
|
versionPath: '/docs',
|
||||||
|
};
|
||||||
|
|
||||||
|
const v100: VersionMetadata = {
|
||||||
|
docsDirPath: path.join(versionedSiteDir, 'versioned_docs/version-1.0.0'),
|
||||||
|
isLast: false,
|
||||||
|
routePriority: undefined,
|
||||||
|
sidebarFilePath: path.join(
|
||||||
|
versionedSiteDir,
|
||||||
|
'versioned_sidebars/version-1.0.0-sidebars.json',
|
||||||
|
),
|
||||||
|
versionLabel: '1.0.0',
|
||||||
|
versionName: '1.0.0',
|
||||||
|
versionPath: '/docs/1.0.0',
|
||||||
|
};
|
||||||
|
|
||||||
|
const vwithSlugs: VersionMetadata = {
|
||||||
|
docsDirPath: path.join(
|
||||||
|
versionedSiteDir,
|
||||||
|
'versioned_docs/version-withSlugs',
|
||||||
|
),
|
||||||
|
isLast: false,
|
||||||
|
routePriority: undefined,
|
||||||
|
sidebarFilePath: path.join(
|
||||||
|
versionedSiteDir,
|
||||||
|
'versioned_sidebars/version-withSlugs-sidebars.json',
|
||||||
|
),
|
||||||
|
versionLabel: 'withSlugs',
|
||||||
|
versionName: 'withSlugs',
|
||||||
|
versionPath: '/docs/withSlugs',
|
||||||
|
};
|
||||||
|
|
||||||
|
test('readVersionsMetadata versioned site', () => {
|
||||||
|
const versionsMetadata = readVersionsMetadata({
|
||||||
|
options: defaultOptions,
|
||||||
|
context: defaultContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(versionsMetadata).toEqual([vCurrent, v101, v100, vwithSlugs]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('readVersionsMetadata versioned site with includeCurrentVersion=false', () => {
|
||||||
|
const versionsMetadata = readVersionsMetadata({
|
||||||
|
options: {...defaultOptions, includeCurrentVersion: false},
|
||||||
|
context: defaultContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(versionsMetadata).toEqual([
|
||||||
|
// vCurrent removed
|
||||||
|
v101,
|
||||||
|
v100,
|
||||||
|
vwithSlugs,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('readVersionsMetadata versioned site with disableVersioning', () => {
|
||||||
|
const versionsMetadata = readVersionsMetadata({
|
||||||
|
options: {...defaultOptions, disableVersioning: true},
|
||||||
|
context: defaultContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(versionsMetadata).toEqual([
|
||||||
|
{...vCurrent, isLast: true, routePriority: -1, versionPath: '/docs'},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('readVersionsMetadata versioned site with all versions disabled', () => {
|
||||||
|
expect(() =>
|
||||||
|
readVersionsMetadata({
|
||||||
|
options: {
|
||||||
|
...defaultOptions,
|
||||||
|
includeCurrentVersion: false,
|
||||||
|
disableVersioning: true,
|
||||||
|
},
|
||||||
|
context: defaultContext,
|
||||||
|
}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"It is not possible to use docs without any version. Please check the configuration of these options: includeCurrentVersion=false disableVersioning=true"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('readVersionsMetadata versioned site with invalid versions.json file', () => {
|
||||||
|
const mock = jest.spyOn(JSON, 'parse').mockImplementationOnce(() => {
|
||||||
|
return {
|
||||||
|
invalid: 'json',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
readVersionsMetadata({
|
||||||
|
options: defaultOptions,
|
||||||
|
context: defaultContext,
|
||||||
|
});
|
||||||
|
}).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"The versions file should contain an array of versions! Found content={\\"invalid\\":\\"json\\"}"`,
|
||||||
|
);
|
||||||
|
mock.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('versioned site, pluginId=community', () => {
|
||||||
|
const versionedSiteDir = path.resolve(
|
||||||
|
path.join(__dirname, '__fixtures__', 'versioned-site'),
|
||||||
|
);
|
||||||
|
const defaultOptions = {
|
||||||
|
...DEFAULT_OPTIONS,
|
||||||
|
id: 'community',
|
||||||
|
path: 'community',
|
||||||
|
routeBasePath: 'communityBasePath',
|
||||||
|
};
|
||||||
|
const defaultContext = {
|
||||||
|
siteDir: versionedSiteDir,
|
||||||
|
baseUrl: '/',
|
||||||
|
};
|
||||||
|
|
||||||
|
const vCurrent: VersionMetadata = {
|
||||||
|
docsDirPath: path.join(versionedSiteDir, 'community'),
|
||||||
|
isLast: false,
|
||||||
|
routePriority: undefined,
|
||||||
|
sidebarFilePath: path.join(versionedSiteDir, 'sidebars.json'),
|
||||||
|
versionLabel: 'Next',
|
||||||
|
versionName: 'current',
|
||||||
|
versionPath: '/communityBasePath/next',
|
||||||
|
};
|
||||||
|
|
||||||
|
const v100: VersionMetadata = {
|
||||||
|
docsDirPath: path.join(
|
||||||
|
versionedSiteDir,
|
||||||
|
'community_versioned_docs/version-1.0.0',
|
||||||
|
),
|
||||||
|
isLast: true,
|
||||||
|
routePriority: -1,
|
||||||
|
sidebarFilePath: path.join(
|
||||||
|
versionedSiteDir,
|
||||||
|
'community_versioned_sidebars/version-1.0.0-sidebars.json',
|
||||||
|
),
|
||||||
|
versionLabel: '1.0.0',
|
||||||
|
versionName: '1.0.0',
|
||||||
|
versionPath: '/communityBasePath',
|
||||||
|
};
|
||||||
|
|
||||||
|
test('readVersionsMetadata versioned site (community)', () => {
|
||||||
|
const versionsMetadata = readVersionsMetadata({
|
||||||
|
options: defaultOptions,
|
||||||
|
context: defaultContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(versionsMetadata).toEqual([vCurrent, v100]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('readVersionsMetadata versioned site (community) with includeCurrentVersion=false', () => {
|
||||||
|
const versionsMetadata = readVersionsMetadata({
|
||||||
|
options: {...defaultOptions, includeCurrentVersion: false},
|
||||||
|
context: defaultContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(versionsMetadata).toEqual([
|
||||||
|
// vCurrent removed
|
||||||
|
v100,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('readVersionsMetadata versioned site (community) with disableVersioning', () => {
|
||||||
|
const versionsMetadata = readVersionsMetadata({
|
||||||
|
options: {...defaultOptions, disableVersioning: true},
|
||||||
|
context: defaultContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(versionsMetadata).toEqual([
|
||||||
|
{
|
||||||
|
...vCurrent,
|
||||||
|
isLast: true,
|
||||||
|
routePriority: -1,
|
||||||
|
versionPath: '/communityBasePath',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('readVersionsMetadata versioned site (community) with all versions disabled', () => {
|
||||||
|
expect(() =>
|
||||||
|
readVersionsMetadata({
|
||||||
|
options: {
|
||||||
|
...defaultOptions,
|
||||||
|
includeCurrentVersion: false,
|
||||||
|
disableVersioning: true,
|
||||||
|
},
|
||||||
|
context: defaultContext,
|
||||||
|
}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"It is not possible to use docs without any version. Please check the configuration of these options: includeCurrentVersion=false disableVersioning=true"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -6,19 +6,19 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getVersionsJSONFile,
|
getVersionsFilePath,
|
||||||
getVersionedDocsDir,
|
getVersionedDocsDirPath,
|
||||||
getVersionedSidebarsDir,
|
getVersionedSidebarsDirPath,
|
||||||
} from './env';
|
} from './versions';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {Sidebar, PathOptions, SidebarItem} from './types';
|
import {Sidebars, PathOptions, SidebarItem} from './types';
|
||||||
import loadSidebars from './sidebars';
|
import {loadSidebars} from './sidebars';
|
||||||
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
|
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
|
||||||
|
|
||||||
// Tests depend on non-default export for mocking.
|
// Tests depend on non-default export for mocking.
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
export function docsVersion(
|
export function cliDocsVersionCommand(
|
||||||
version: string | null | undefined,
|
version: string | null | undefined,
|
||||||
siteDir: string,
|
siteDir: string,
|
||||||
pluginId: string,
|
pluginId: string,
|
||||||
|
@ -63,7 +63,7 @@ export function docsVersion(
|
||||||
|
|
||||||
// Load existing versions.
|
// Load existing versions.
|
||||||
let versions = [];
|
let versions = [];
|
||||||
const versionsJSONFile = getVersionsJSONFile(siteDir, pluginId);
|
const versionsJSONFile = getVersionsFilePath(siteDir, pluginId);
|
||||||
if (fs.existsSync(versionsJSONFile)) {
|
if (fs.existsSync(versionsJSONFile)) {
|
||||||
versions = JSON.parse(fs.readFileSync(versionsJSONFile, 'utf8'));
|
versions = JSON.parse(fs.readFileSync(versionsJSONFile, 'utf8'));
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,7 @@ export function docsVersion(
|
||||||
// Copy docs files.
|
// Copy docs files.
|
||||||
const docsDir = path.join(siteDir, docsPath);
|
const docsDir = path.join(siteDir, docsPath);
|
||||||
if (fs.existsSync(docsDir) && fs.readdirSync(docsDir).length > 0) {
|
if (fs.existsSync(docsDir) && fs.readdirSync(docsDir).length > 0) {
|
||||||
const versionedDir = getVersionedDocsDir(siteDir, pluginId);
|
const versionedDir = getVersionedDocsDirPath(siteDir, pluginId);
|
||||||
const newVersionDir = path.join(versionedDir, `version-${version}`);
|
const newVersionDir = path.join(versionedDir, `version-${version}`);
|
||||||
fs.copySync(docsDir, newVersionDir);
|
fs.copySync(docsDir, newVersionDir);
|
||||||
} else {
|
} else {
|
||||||
|
@ -89,7 +89,7 @@ export function docsVersion(
|
||||||
|
|
||||||
// Load current sidebar and create a new versioned sidebars file.
|
// Load current sidebar and create a new versioned sidebars file.
|
||||||
if (fs.existsSync(sidebarPath)) {
|
if (fs.existsSync(sidebarPath)) {
|
||||||
const loadedSidebars: Sidebar = loadSidebars([sidebarPath]);
|
const loadedSidebars: Sidebars = loadSidebars(sidebarPath);
|
||||||
|
|
||||||
// Transform id in original sidebar to versioned id.
|
// Transform id in original sidebar to versioned id.
|
||||||
const normalizeItem = (item: SidebarItem): SidebarItem => {
|
const normalizeItem = (item: SidebarItem): SidebarItem => {
|
||||||
|
@ -107,8 +107,8 @@ export function docsVersion(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const versionedSidebar: Sidebar = Object.entries(loadedSidebars).reduce(
|
const versionedSidebar: Sidebars = Object.entries(loadedSidebars).reduce(
|
||||||
(acc: Sidebar, [sidebarId, sidebarItems]) => {
|
(acc: Sidebars, [sidebarId, sidebarItems]) => {
|
||||||
const newVersionedSidebarId = `version-${version}/${sidebarId}`;
|
const newVersionedSidebarId = `version-${version}/${sidebarId}`;
|
||||||
acc[newVersionedSidebarId] = sidebarItems.map(normalizeItem);
|
acc[newVersionedSidebarId] = sidebarItems.map(normalizeItem);
|
||||||
return acc;
|
return acc;
|
||||||
|
@ -116,7 +116,7 @@ export function docsVersion(
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
const versionedSidebarsDir = getVersionedSidebarsDir(siteDir, pluginId);
|
const versionedSidebarsDir = getVersionedSidebarsDirPath(siteDir, pluginId);
|
||||||
const newSidebarFile = path.join(
|
const newSidebarFile = path.join(
|
||||||
versionedSidebarsDir,
|
versionedSidebarsDir,
|
||||||
`version-${version}-sidebars.json`,
|
`version-${version}-sidebars.json`,
|
|
@ -12,7 +12,7 @@ import {
|
||||||
getActiveDocContext,
|
getActiveDocContext,
|
||||||
getActiveVersion,
|
getActiveVersion,
|
||||||
getDocVersionSuggestions,
|
getDocVersionSuggestions,
|
||||||
} from '../../client/docsClientUtils';
|
} from '../docsClientUtils';
|
||||||
import {GlobalPluginData, GlobalVersion} from '../../types';
|
import {GlobalPluginData, GlobalVersion} from '../../types';
|
||||||
import {shuffle} from 'lodash';
|
import {shuffle} from 'lodash';
|
||||||
|
|
||||||
|
@ -21,12 +21,10 @@ describe('docsClientUtils', () => {
|
||||||
const data: Record<string, GlobalPluginData> = {
|
const data: Record<string, GlobalPluginData> = {
|
||||||
pluginIosId: {
|
pluginIosId: {
|
||||||
path: '/ios',
|
path: '/ios',
|
||||||
latestVersionName: 'xyz',
|
|
||||||
versions: [],
|
versions: [],
|
||||||
},
|
},
|
||||||
pluginAndroidId: {
|
pluginAndroidId: {
|
||||||
path: '/android',
|
path: '/android',
|
||||||
latestVersionName: 'xyz',
|
|
||||||
versions: [],
|
versions: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -55,19 +53,25 @@ describe('docsClientUtils', () => {
|
||||||
const versions: GlobalVersion[] = [
|
const versions: GlobalVersion[] = [
|
||||||
{
|
{
|
||||||
name: 'version1',
|
name: 'version1',
|
||||||
|
label: 'version1',
|
||||||
path: '/???',
|
path: '/???',
|
||||||
|
isLast: false,
|
||||||
docs: [],
|
docs: [],
|
||||||
mainDocId: '???',
|
mainDocId: '???',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'version2',
|
name: 'version2',
|
||||||
|
label: 'version2',
|
||||||
path: '/???',
|
path: '/???',
|
||||||
|
isLast: true,
|
||||||
docs: [],
|
docs: [],
|
||||||
mainDocId: '???',
|
mainDocId: '???',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'version3',
|
name: 'version3',
|
||||||
|
label: 'version3',
|
||||||
path: '/???',
|
path: '/???',
|
||||||
|
isLast: false,
|
||||||
docs: [],
|
docs: [],
|
||||||
mainDocId: '???',
|
mainDocId: '???',
|
||||||
},
|
},
|
||||||
|
@ -76,52 +80,35 @@ describe('docsClientUtils', () => {
|
||||||
expect(
|
expect(
|
||||||
getLatestVersion({
|
getLatestVersion({
|
||||||
path: '???',
|
path: '???',
|
||||||
latestVersionName: 'does not exist',
|
|
||||||
versions,
|
versions,
|
||||||
}),
|
}),
|
||||||
).toEqual(undefined);
|
).toEqual(versions[1]);
|
||||||
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', () => {
|
test('getActiveVersion', () => {
|
||||||
const data: GlobalPluginData = {
|
const data: GlobalPluginData = {
|
||||||
path: 'docs',
|
path: 'docs',
|
||||||
latestVersionName: 'version2',
|
|
||||||
versions: [
|
versions: [
|
||||||
{
|
{
|
||||||
name: 'next',
|
name: 'next',
|
||||||
|
label: 'next',
|
||||||
|
isLast: false,
|
||||||
path: '/docs/next',
|
path: '/docs/next',
|
||||||
docs: [],
|
docs: [],
|
||||||
mainDocId: '???',
|
mainDocId: '???',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'version2',
|
name: 'version2',
|
||||||
|
label: 'version2',
|
||||||
|
isLast: true,
|
||||||
path: '/docs',
|
path: '/docs',
|
||||||
docs: [],
|
docs: [],
|
||||||
mainDocId: '???',
|
mainDocId: '???',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'version1',
|
name: 'version1',
|
||||||
|
label: 'version1',
|
||||||
|
isLast: false,
|
||||||
path: '/docs/version1',
|
path: '/docs/version1',
|
||||||
docs: [],
|
docs: [],
|
||||||
mainDocId: '???',
|
mainDocId: '???',
|
||||||
|
@ -146,7 +133,9 @@ describe('docsClientUtils', () => {
|
||||||
test('getActiveDocContext', () => {
|
test('getActiveDocContext', () => {
|
||||||
const versionNext: GlobalVersion = {
|
const versionNext: GlobalVersion = {
|
||||||
name: 'next',
|
name: 'next',
|
||||||
|
label: 'next',
|
||||||
path: '/docs/next',
|
path: '/docs/next',
|
||||||
|
isLast: false,
|
||||||
mainDocId: 'doc1',
|
mainDocId: 'doc1',
|
||||||
docs: [
|
docs: [
|
||||||
{
|
{
|
||||||
|
@ -162,6 +151,8 @@ describe('docsClientUtils', () => {
|
||||||
|
|
||||||
const version2: GlobalVersion = {
|
const version2: GlobalVersion = {
|
||||||
name: 'version2',
|
name: 'version2',
|
||||||
|
label: 'version2',
|
||||||
|
isLast: true,
|
||||||
path: '/docs',
|
path: '/docs',
|
||||||
mainDocId: 'doc1',
|
mainDocId: 'doc1',
|
||||||
docs: [
|
docs: [
|
||||||
|
@ -178,7 +169,9 @@ describe('docsClientUtils', () => {
|
||||||
|
|
||||||
const version1: GlobalVersion = {
|
const version1: GlobalVersion = {
|
||||||
name: 'version1',
|
name: 'version1',
|
||||||
|
label: 'version1',
|
||||||
path: '/docs/version1',
|
path: '/docs/version1',
|
||||||
|
isLast: false,
|
||||||
mainDocId: 'doc1',
|
mainDocId: 'doc1',
|
||||||
docs: [
|
docs: [
|
||||||
{
|
{
|
||||||
|
@ -197,7 +190,6 @@ describe('docsClientUtils', () => {
|
||||||
|
|
||||||
const data: GlobalPluginData = {
|
const data: GlobalPluginData = {
|
||||||
path: 'docs',
|
path: 'docs',
|
||||||
latestVersionName: 'version2',
|
|
||||||
versions,
|
versions,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -270,6 +262,8 @@ describe('docsClientUtils', () => {
|
||||||
test('getDocVersionSuggestions', () => {
|
test('getDocVersionSuggestions', () => {
|
||||||
const versionNext: GlobalVersion = {
|
const versionNext: GlobalVersion = {
|
||||||
name: 'next',
|
name: 'next',
|
||||||
|
label: 'next',
|
||||||
|
isLast: false,
|
||||||
path: '/docs/next',
|
path: '/docs/next',
|
||||||
mainDocId: 'doc1',
|
mainDocId: 'doc1',
|
||||||
docs: [
|
docs: [
|
||||||
|
@ -286,7 +280,9 @@ describe('docsClientUtils', () => {
|
||||||
|
|
||||||
const version2: GlobalVersion = {
|
const version2: GlobalVersion = {
|
||||||
name: 'version2',
|
name: 'version2',
|
||||||
|
label: 'version2',
|
||||||
path: '/docs',
|
path: '/docs',
|
||||||
|
isLast: true,
|
||||||
mainDocId: 'doc1',
|
mainDocId: 'doc1',
|
||||||
docs: [
|
docs: [
|
||||||
{
|
{
|
||||||
|
@ -302,6 +298,8 @@ describe('docsClientUtils', () => {
|
||||||
|
|
||||||
const version1: GlobalVersion = {
|
const version1: GlobalVersion = {
|
||||||
name: 'version1',
|
name: 'version1',
|
||||||
|
label: 'version1',
|
||||||
|
isLast: false,
|
||||||
path: '/docs/version1',
|
path: '/docs/version1',
|
||||||
mainDocId: 'doc1',
|
mainDocId: 'doc1',
|
||||||
docs: [
|
docs: [
|
||||||
|
@ -321,7 +319,6 @@ describe('docsClientUtils', () => {
|
||||||
|
|
||||||
const data: GlobalPluginData = {
|
const data: GlobalPluginData = {
|
||||||
path: 'docs',
|
path: 'docs',
|
||||||
latestVersionName: 'version2',
|
|
||||||
versions,
|
versions,
|
||||||
};
|
};
|
||||||
|
|
|
@ -49,9 +49,7 @@ export type ActiveDocContext = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLatestVersion = (data: GlobalPluginData): Version => {
|
export const getLatestVersion = (data: GlobalPluginData): Version => {
|
||||||
return data.versions.find(
|
return data.versions.find((version) => version.isLast)!;
|
||||||
(version) => version.name === data.latestVersionName,
|
|
||||||
)!;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Note: return undefined on doc-unrelated pages,
|
// Note: return undefined on doc-unrelated pages,
|
||||||
|
|
|
@ -5,6 +5,9 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// The name of the version at the root of your site (website/docs)
|
||||||
|
export const CURRENT_VERSION_NAME = 'current';
|
||||||
|
|
||||||
export const VERSIONED_DOCS_DIR = 'versioned_docs';
|
export const VERSIONED_DOCS_DIR = 'versioned_docs';
|
||||||
export const VERSIONED_SIDEBARS_DIR = 'versioned_sidebars';
|
export const VERSIONED_SIDEBARS_DIR = 'versioned_sidebars';
|
||||||
export const VERSIONS_JSON_FILE = 'versions.json';
|
export const VERSIONS_JSON_FILE = 'versions.json';
|
||||||
|
|
185
packages/docusaurus-plugin-content-docs/src/docs.ts
Normal file
185
packages/docusaurus-plugin-content-docs/src/docs.ts
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
/**
|
||||||
|
* 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 path from 'path';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import {
|
||||||
|
aliasedSitePath,
|
||||||
|
normalizeUrl,
|
||||||
|
getEditUrl,
|
||||||
|
parseMarkdownString,
|
||||||
|
} from '@docusaurus/utils';
|
||||||
|
import {LoadContext} from '@docusaurus/types';
|
||||||
|
|
||||||
|
import {getFileLastUpdate} from './lastUpdate';
|
||||||
|
import {
|
||||||
|
DocMetadataBase,
|
||||||
|
LastUpdateData,
|
||||||
|
MetadataOptions,
|
||||||
|
VersionMetadata,
|
||||||
|
DocFile,
|
||||||
|
PluginOptions,
|
||||||
|
} from './types';
|
||||||
|
import getSlug from './slug';
|
||||||
|
import {CURRENT_VERSION_NAME} from './constants';
|
||||||
|
import globby from 'globby';
|
||||||
|
|
||||||
|
type LastUpdateOptions = Pick<
|
||||||
|
PluginOptions,
|
||||||
|
'showLastUpdateAuthor' | 'showLastUpdateTime'
|
||||||
|
>;
|
||||||
|
|
||||||
|
async function readLastUpdateData(
|
||||||
|
filePath: string,
|
||||||
|
options: LastUpdateOptions,
|
||||||
|
): Promise<LastUpdateData> {
|
||||||
|
const {showLastUpdateAuthor, showLastUpdateTime} = options;
|
||||||
|
if (showLastUpdateAuthor || showLastUpdateTime) {
|
||||||
|
// Use fake data in dev for faster development.
|
||||||
|
const fileLastUpdateData =
|
||||||
|
process.env.NODE_ENV === 'production'
|
||||||
|
? await getFileLastUpdate(filePath)
|
||||||
|
: {
|
||||||
|
author: 'Author',
|
||||||
|
timestamp: 1539502055,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (fileLastUpdateData) {
|
||||||
|
const {author, timestamp} = fileLastUpdateData;
|
||||||
|
return {
|
||||||
|
lastUpdatedAt: showLastUpdateTime ? timestamp : undefined,
|
||||||
|
lastUpdatedBy: showLastUpdateAuthor ? author : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readDocFile(
|
||||||
|
docsDirPath: string,
|
||||||
|
source: string,
|
||||||
|
options: LastUpdateOptions,
|
||||||
|
): Promise<DocFile> {
|
||||||
|
const filePath = path.join(docsDirPath, source);
|
||||||
|
const [content, lastUpdate] = await Promise.all([
|
||||||
|
fs.readFile(filePath, 'utf-8'),
|
||||||
|
readLastUpdateData(filePath, options),
|
||||||
|
]);
|
||||||
|
return {source, content, lastUpdate};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readVersionDocs(
|
||||||
|
versionMetadata: VersionMetadata,
|
||||||
|
options: Pick<
|
||||||
|
PluginOptions,
|
||||||
|
'include' | 'showLastUpdateAuthor' | 'showLastUpdateTime'
|
||||||
|
>,
|
||||||
|
): Promise<DocFile[]> {
|
||||||
|
const sources = await globby(options.include, {
|
||||||
|
cwd: versionMetadata.docsDirPath,
|
||||||
|
});
|
||||||
|
return Promise.all(
|
||||||
|
sources.map((source) =>
|
||||||
|
readDocFile(versionMetadata.docsDirPath, source, options),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processDocMetadata({
|
||||||
|
docFile,
|
||||||
|
versionMetadata,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
docFile: DocFile;
|
||||||
|
versionMetadata: VersionMetadata;
|
||||||
|
context: LoadContext;
|
||||||
|
options: MetadataOptions;
|
||||||
|
}): DocMetadataBase {
|
||||||
|
const {source, content, lastUpdate} = docFile;
|
||||||
|
const {editUrl, homePageId} = options;
|
||||||
|
const {siteDir} = context;
|
||||||
|
const filePath = path.join(versionMetadata.docsDirPath, source);
|
||||||
|
|
||||||
|
// ex: api/myDoc -> api
|
||||||
|
// ex: myDoc -> .
|
||||||
|
const docsFileDirName = path.dirname(source);
|
||||||
|
|
||||||
|
const docsEditUrl = getEditUrl(path.relative(siteDir, filePath), editUrl);
|
||||||
|
|
||||||
|
const {frontMatter = {}, excerpt} = parseMarkdownString(content);
|
||||||
|
const {sidebar_label, custom_edit_url} = frontMatter;
|
||||||
|
|
||||||
|
const baseID: string =
|
||||||
|
frontMatter.id || path.basename(source, path.extname(source));
|
||||||
|
if (baseID.includes('/')) {
|
||||||
|
throw new Error(`Document id [${baseID}] cannot include "/".`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO legacy retrocompatibility
|
||||||
|
// The same doc in 2 distinct version could keep the same id,
|
||||||
|
// we just need to namespace the data by version
|
||||||
|
const versionIdPart =
|
||||||
|
versionMetadata.versionName === CURRENT_VERSION_NAME
|
||||||
|
? ''
|
||||||
|
: `version-${versionMetadata.versionName}/`;
|
||||||
|
|
||||||
|
// TODO legacy retrocompatibility
|
||||||
|
// I think it's bad to affect the frontmatter id with the dirname
|
||||||
|
const dirNameIdPart = docsFileDirName === '.' ? '' : `${docsFileDirName}/`;
|
||||||
|
|
||||||
|
// TODO legacy composite id, requires a breaking change to modify this
|
||||||
|
const id = `${versionIdPart}${dirNameIdPart}${baseID}`;
|
||||||
|
|
||||||
|
const unversionedId = `${dirNameIdPart}${baseID}`;
|
||||||
|
|
||||||
|
// TODO remove soon, deprecated homePageId
|
||||||
|
const isDocsHomePage = unversionedId === (homePageId ?? '_index');
|
||||||
|
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`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const docSlug = isDocsHomePage
|
||||||
|
? '/'
|
||||||
|
: getSlug({
|
||||||
|
baseID,
|
||||||
|
dirName: docsFileDirName,
|
||||||
|
frontmatterSlug: frontMatter.slug,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Default title is the id.
|
||||||
|
const title: string = frontMatter.title || baseID;
|
||||||
|
|
||||||
|
const description: string = frontMatter.description || excerpt;
|
||||||
|
|
||||||
|
const permalink = normalizeUrl([versionMetadata.versionPath, docSlug]);
|
||||||
|
|
||||||
|
// Assign all of object properties during instantiation (if possible) for
|
||||||
|
// NodeJS optimization.
|
||||||
|
// Adding properties to object after instantiation will cause hidden
|
||||||
|
// class transitions.
|
||||||
|
const metadata: DocMetadataBase = {
|
||||||
|
unversionedId,
|
||||||
|
id,
|
||||||
|
isDocsHomePage,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
source: aliasedSitePath(filePath, siteDir),
|
||||||
|
slug: docSlug,
|
||||||
|
permalink,
|
||||||
|
editUrl: custom_edit_url !== undefined ? custom_edit_url : docsEditUrl,
|
||||||
|
version: versionMetadata.versionName,
|
||||||
|
lastUpdatedBy: lastUpdate.lastUpdatedBy,
|
||||||
|
lastUpdatedAt: lastUpdate.lastUpdatedAt,
|
||||||
|
sidebar_label,
|
||||||
|
};
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
|
@ -1,88 +0,0 @@
|
||||||
/**
|
|
||||||
* 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 path from 'path';
|
|
||||||
import fs from 'fs-extra';
|
|
||||||
import {VersioningEnv, Env} from './types';
|
|
||||||
import {
|
|
||||||
VERSIONS_JSON_FILE,
|
|
||||||
VERSIONED_DOCS_DIR,
|
|
||||||
VERSIONED_SIDEBARS_DIR,
|
|
||||||
} from './constants';
|
|
||||||
|
|
||||||
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
|
|
||||||
|
|
||||||
// retro-compatibility: no prefix for the default plugin id
|
|
||||||
function addPluginIdPrefix(fileOrDir: string, pluginId: string): string {
|
|
||||||
if (pluginId === DEFAULT_PLUGIN_ID) {
|
|
||||||
return fileOrDir;
|
|
||||||
} else {
|
|
||||||
return `${pluginId}_${fileOrDir}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getVersionedDocsDir(siteDir: string, pluginId: string): string {
|
|
||||||
return path.join(siteDir, addPluginIdPrefix(VERSIONED_DOCS_DIR, pluginId));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getVersionedSidebarsDir(
|
|
||||||
siteDir: string,
|
|
||||||
pluginId: string,
|
|
||||||
): string {
|
|
||||||
return path.join(
|
|
||||||
siteDir,
|
|
||||||
addPluginIdPrefix(VERSIONED_SIDEBARS_DIR, pluginId),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getVersionsJSONFile(siteDir: string, pluginId: string): string {
|
|
||||||
return path.join(siteDir, addPluginIdPrefix(VERSIONS_JSON_FILE, pluginId));
|
|
||||||
}
|
|
||||||
|
|
||||||
type EnvOptions = Partial<{disableVersioning: boolean}>;
|
|
||||||
|
|
||||||
export default function (
|
|
||||||
siteDir: string,
|
|
||||||
pluginId: string,
|
|
||||||
options: EnvOptions = {disableVersioning: false},
|
|
||||||
): Env {
|
|
||||||
if (!siteDir) {
|
|
||||||
throw new Error('unexpected, missing siteDir');
|
|
||||||
}
|
|
||||||
if (!pluginId) {
|
|
||||||
throw new Error('unexpected, missing pluginId');
|
|
||||||
}
|
|
||||||
|
|
||||||
const versioning: VersioningEnv = {
|
|
||||||
enabled: false,
|
|
||||||
versions: [],
|
|
||||||
latestVersion: null,
|
|
||||||
docsDir: '',
|
|
||||||
sidebarsDir: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
const versionsJSONFile = getVersionsJSONFile(siteDir, pluginId);
|
|
||||||
if (fs.existsSync(versionsJSONFile)) {
|
|
||||||
if (!options.disableVersioning) {
|
|
||||||
const parsedVersions = JSON.parse(
|
|
||||||
fs.readFileSync(versionsJSONFile, 'utf8'),
|
|
||||||
);
|
|
||||||
if (parsedVersions && parsedVersions.length > 0) {
|
|
||||||
// eslint-disable-next-line prefer-destructuring
|
|
||||||
versioning.latestVersion = parsedVersions[0];
|
|
||||||
versioning.enabled = true;
|
|
||||||
versioning.versions = parsedVersions;
|
|
||||||
versioning.docsDir = getVersionedDocsDir(siteDir, pluginId);
|
|
||||||
versioning.sidebarsDir = getVersionedSidebarsDir(siteDir, pluginId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
versioning,
|
|
||||||
};
|
|
||||||
}
|
|
26
packages/docusaurus-plugin-content-docs/src/globalData.ts
Normal file
26
packages/docusaurus-plugin-content-docs/src/globalData.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
/**
|
||||||
|
* 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 {DocMetadata, GlobalDoc, LoadedVersion, GlobalVersion} from './types';
|
||||||
|
|
||||||
|
export function toGlobalDataDoc(doc: DocMetadata): GlobalDoc {
|
||||||
|
return {
|
||||||
|
id: doc.unversionedId,
|
||||||
|
path: doc.permalink,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toGlobalDataVersion(version: LoadedVersion): GlobalVersion {
|
||||||
|
return {
|
||||||
|
name: version.versionName,
|
||||||
|
label: version.versionLabel,
|
||||||
|
isLast: version.isLast,
|
||||||
|
path: version.versionPath,
|
||||||
|
mainDocId: version.mainDocId,
|
||||||
|
docs: version.docs.map(toGlobalDataDoc),
|
||||||
|
};
|
||||||
|
}
|
|
@ -5,93 +5,52 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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';
|
import path from 'path';
|
||||||
import chalk from 'chalk';
|
|
||||||
|
|
||||||
import admonitions from 'remark-admonitions';
|
|
||||||
import {
|
import {
|
||||||
STATIC_DIR_NAME,
|
STATIC_DIR_NAME,
|
||||||
DEFAULT_PLUGIN_ID,
|
DEFAULT_PLUGIN_ID,
|
||||||
} from '@docusaurus/core/lib/constants';
|
} from '@docusaurus/core/lib/constants';
|
||||||
import {
|
import {normalizeUrl, docuHash, aliasedSitePath} from '@docusaurus/utils';
|
||||||
normalizeUrl,
|
import {LoadContext, Plugin, RouteConfig} from '@docusaurus/types';
|
||||||
docuHash,
|
|
||||||
objectWithKeySorted,
|
|
||||||
aliasedSitePath,
|
|
||||||
} from '@docusaurus/utils';
|
|
||||||
import {
|
|
||||||
LoadContext,
|
|
||||||
Plugin,
|
|
||||||
RouteConfig,
|
|
||||||
OptionValidationContext,
|
|
||||||
ValidationResult,
|
|
||||||
} from '@docusaurus/types';
|
|
||||||
|
|
||||||
import createOrder from './order';
|
import {loadSidebars, createSidebarsUtils} from './sidebars';
|
||||||
import loadSidebars from './sidebars';
|
import {readVersionDocs, processDocMetadata} from './docs';
|
||||||
import processMetadata from './metadata';
|
import {readVersionsMetadata} from './versions';
|
||||||
import loadEnv from './env';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PluginOptions,
|
PluginOptions,
|
||||||
Sidebar,
|
|
||||||
Order,
|
|
||||||
DocsMetadata,
|
|
||||||
LoadedContent,
|
LoadedContent,
|
||||||
SourceToPermalink,
|
SourceToPermalink,
|
||||||
PermalinkToSidebar,
|
PermalinkToSidebar,
|
||||||
SidebarItemLink,
|
DocMetadataBase,
|
||||||
SidebarItemDoc,
|
DocMetadata,
|
||||||
DocsSidebar,
|
|
||||||
DocsBaseMetadata,
|
|
||||||
MetadataRaw,
|
|
||||||
DocsMetadataRaw,
|
|
||||||
Metadata,
|
|
||||||
VersionToSidebars,
|
|
||||||
SidebarItem,
|
|
||||||
DocsSidebarItem,
|
|
||||||
GlobalPluginData,
|
GlobalPluginData,
|
||||||
DocsVersion,
|
VersionMetadata,
|
||||||
GlobalVersion,
|
DocNavLink,
|
||||||
GlobalDoc,
|
LoadedVersion,
|
||||||
|
DocFile,
|
||||||
|
DocsMarkdownOption,
|
||||||
} from './types';
|
} from './types';
|
||||||
import {Configuration} from 'webpack';
|
import {RuleSetRule} from 'webpack';
|
||||||
import {docsVersion} from './version';
|
import {cliDocsVersionCommand} from './cli';
|
||||||
import {VERSIONS_JSON_FILE} from './constants';
|
import {VERSIONS_JSON_FILE} from './constants';
|
||||||
import {PluginOptionSchema} from './pluginOptionSchema';
|
import {OptionsSchema} from './options';
|
||||||
import {ValidationError} from '@hapi/joi';
|
import {flatten, keyBy, compact} from 'lodash';
|
||||||
|
import {toGlobalDataVersion} from './globalData';
|
||||||
|
import {toVersionMetadataProp} from './props';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
export default function pluginContentDocs(
|
export default function pluginContentDocs(
|
||||||
context: LoadContext,
|
context: LoadContext,
|
||||||
options: PluginOptions,
|
options: PluginOptions,
|
||||||
): Plugin<LoadedContent | null, typeof PluginOptionSchema> {
|
): Plugin<LoadedContent, typeof OptionsSchema> {
|
||||||
// TODO remove homePageId before end of 2020
|
|
||||||
// "slug: /" is better because the home doc can be different across versions
|
|
||||||
if (options.homePageId) {
|
|
||||||
console.log(
|
|
||||||
chalk.red(
|
|
||||||
`The docs plugin option homePageId=${options.homePageId} is deprecated. To make a doc the "home", prefer frontmatter: "slug: /"`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.admonitions) {
|
|
||||||
options.remarkPlugins = options.remarkPlugins.concat([
|
|
||||||
[admonitions, options.admonitions],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {siteDir, generatedFilesDir, baseUrl} = context;
|
const {siteDir, generatedFilesDir, baseUrl} = context;
|
||||||
const docsDir = path.resolve(siteDir, options.path);
|
|
||||||
|
const versionsMetadata = readVersionsMetadata({context, options});
|
||||||
|
|
||||||
const sourceToPermalink: SourceToPermalink = {};
|
const sourceToPermalink: SourceToPermalink = {};
|
||||||
const pluginId = options.id ?? DEFAULT_PLUGIN_ID;
|
const pluginId = options.id ?? DEFAULT_PLUGIN_ID;
|
||||||
const isDefaultPluginId = pluginId === DEFAULT_PLUGIN_ID;
|
|
||||||
|
|
||||||
const pluginDataDirRoot = path.join(
|
const pluginDataDirRoot = path.join(
|
||||||
generatedFilesDir,
|
generatedFilesDir,
|
||||||
|
@ -101,18 +60,6 @@ export default function pluginContentDocs(
|
||||||
const aliasedSource = (source: string) =>
|
const aliasedSource = (source: string) =>
|
||||||
`~docs/${path.relative(pluginDataDirRoot, source)}`;
|
`~docs/${path.relative(pluginDataDirRoot, source)}`;
|
||||||
|
|
||||||
// Versioning.
|
|
||||||
const env = loadEnv(siteDir, pluginId, {
|
|
||||||
disableVersioning: options.disableVersioning,
|
|
||||||
});
|
|
||||||
const {versioning} = env;
|
|
||||||
const {
|
|
||||||
versions,
|
|
||||||
docsDir: versionedDir,
|
|
||||||
sidebarsDir: versionedSidebarsDir,
|
|
||||||
} = versioning;
|
|
||||||
const versionsNames = versions.map((version) => `version-${version}`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'docusaurus-plugin-content-docs',
|
name: 'docusaurus-plugin-content-docs',
|
||||||
|
|
||||||
|
@ -125,6 +72,10 @@ export default function pluginContentDocs(
|
||||||
},
|
},
|
||||||
|
|
||||||
extendCli(cli) {
|
extendCli(cli) {
|
||||||
|
const isDefaultPluginId = pluginId === DEFAULT_PLUGIN_ID;
|
||||||
|
|
||||||
|
// Need to create one distinct command per plugin instance
|
||||||
|
// otherwise 2 instances would try to execute the command!
|
||||||
const command = isDefaultPluginId
|
const command = isDefaultPluginId
|
||||||
? 'docs:version'
|
? 'docs:version'
|
||||||
: `docs:version:${pluginId}`;
|
: `docs:version:${pluginId}`;
|
||||||
|
@ -137,259 +88,159 @@ export default function pluginContentDocs(
|
||||||
.arguments('<version>')
|
.arguments('<version>')
|
||||||
.description(commandDescription)
|
.description(commandDescription)
|
||||||
.action((version) => {
|
.action((version) => {
|
||||||
docsVersion(version, siteDir, pluginId, {
|
cliDocsVersionCommand(version, siteDir, pluginId, {
|
||||||
path: options.path,
|
path: options.path,
|
||||||
sidebarPath: options.sidebarPath,
|
sidebarPath: options.sidebarPath,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getPathsToWatch() {
|
|
||||||
const {include} = options;
|
|
||||||
let globPattern = include.map((pattern) => `${docsDir}/${pattern}`);
|
|
||||||
if (versioning.enabled) {
|
|
||||||
const docsGlob = include
|
|
||||||
.map((pattern) =>
|
|
||||||
versionsNames.map(
|
|
||||||
(versionName) => `${versionedDir}/${versionName}/${pattern}`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.reduce((a, b) => a.concat(b), []);
|
|
||||||
const sidebarsGlob = versionsNames.map(
|
|
||||||
(versionName) =>
|
|
||||||
`${versionedSidebarsDir}/${versionName}-sidebars.json`,
|
|
||||||
);
|
|
||||||
globPattern = [...globPattern, ...sidebarsGlob, ...docsGlob];
|
|
||||||
}
|
|
||||||
return [...globPattern, options.sidebarPath];
|
|
||||||
},
|
|
||||||
|
|
||||||
getClientModules() {
|
getClientModules() {
|
||||||
const modules = [];
|
const modules = [];
|
||||||
|
|
||||||
if (options.admonitions) {
|
if (options.admonitions) {
|
||||||
modules.push(require.resolve('remark-admonitions/styles/infima.css'));
|
modules.push(require.resolve('remark-admonitions/styles/infima.css'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return modules;
|
return modules;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Fetches blog contents and returns metadata for the contents.
|
getPathsToWatch() {
|
||||||
|
function getVersionPathsToWatch(version: VersionMetadata): string[] {
|
||||||
|
return [
|
||||||
|
version.sidebarFilePath,
|
||||||
|
...options.include.map(
|
||||||
|
(pattern) => `${version.docsDirPath}/${pattern}`,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return flatten(versionsMetadata.map(getVersionPathsToWatch));
|
||||||
|
},
|
||||||
|
|
||||||
async loadContent() {
|
async loadContent() {
|
||||||
const {include, sidebarPath} = options;
|
async function loadVersionDocsBase(
|
||||||
|
versionMetadata: VersionMetadata,
|
||||||
if (!fs.existsSync(docsDir)) {
|
): Promise<DocMetadataBase[]> {
|
||||||
console.error(
|
const docFiles = await readVersionDocs(versionMetadata, options);
|
||||||
chalk.red(
|
if (docFiles.length === 0) {
|
||||||
`No docs directory found for the docs plugin at: ${docsDir}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare metadata container.
|
|
||||||
const docsMetadataRaw: DocsMetadataRaw = {};
|
|
||||||
const docsPromises = [];
|
|
||||||
const includeDefaultDocs = !(
|
|
||||||
options.excludeNextVersionDocs && process.argv[2] === 'build'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Metadata for default/master docs files.
|
|
||||||
if (includeDefaultDocs) {
|
|
||||||
const docsFiles = await globby(include, {
|
|
||||||
cwd: docsDir,
|
|
||||||
});
|
|
||||||
docsPromises.push(
|
|
||||||
Promise.all(
|
|
||||||
docsFiles.map(async (source) => {
|
|
||||||
const metadata: MetadataRaw = await processMetadata({
|
|
||||||
source,
|
|
||||||
refDir: docsDir,
|
|
||||||
context,
|
|
||||||
options,
|
|
||||||
env,
|
|
||||||
});
|
|
||||||
docsMetadataRaw[metadata.id] = metadata;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metadata for versioned docs.
|
|
||||||
if (versioning.enabled) {
|
|
||||||
const versionedGlob = include
|
|
||||||
.map((pattern) =>
|
|
||||||
versionsNames.map((versionName) => `${versionName}/${pattern}`),
|
|
||||||
)
|
|
||||||
.reduce((a, b) => a.concat(b), []);
|
|
||||||
const versionedFiles = await globby(versionedGlob, {
|
|
||||||
cwd: versionedDir,
|
|
||||||
});
|
|
||||||
docsPromises.push(
|
|
||||||
Promise.all(
|
|
||||||
versionedFiles.map(async (source) => {
|
|
||||||
const metadata = await processMetadata({
|
|
||||||
source,
|
|
||||||
refDir: versionedDir,
|
|
||||||
context,
|
|
||||||
options,
|
|
||||||
env,
|
|
||||||
});
|
|
||||||
docsMetadataRaw[metadata.id] = metadata;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the sidebars and create docs ordering.
|
|
||||||
const sidebarPaths = versionsNames.map(
|
|
||||||
(versionName) => `${versionedSidebarsDir}/${versionName}-sidebars.json`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (includeDefaultDocs) {
|
|
||||||
sidebarPaths.unshift(sidebarPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadedSidebars: Sidebar = loadSidebars(sidebarPaths);
|
|
||||||
const order: Order = createOrder(loadedSidebars);
|
|
||||||
|
|
||||||
await Promise.all(docsPromises);
|
|
||||||
|
|
||||||
// Construct inter-metadata relationship in docsMetadata.
|
|
||||||
const docsMetadata: DocsMetadata = {};
|
|
||||||
const permalinkToSidebar: PermalinkToSidebar = {};
|
|
||||||
const versionToSidebars: VersionToSidebars = {};
|
|
||||||
Object.keys(docsMetadataRaw).forEach((currentID) => {
|
|
||||||
const {next: nextID, previous: previousID, sidebar} =
|
|
||||||
order[currentID] || {};
|
|
||||||
const previous = previousID
|
|
||||||
? {
|
|
||||||
title: docsMetadataRaw[previousID]?.title ?? 'Previous',
|
|
||||||
permalink: docsMetadataRaw[previousID]?.permalink,
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
const next = nextID
|
|
||||||
? {
|
|
||||||
title: docsMetadataRaw[nextID]?.title ?? 'Next',
|
|
||||||
permalink: docsMetadataRaw[nextID]?.permalink,
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
docsMetadata[currentID] = {
|
|
||||||
...docsMetadataRaw[currentID],
|
|
||||||
sidebar,
|
|
||||||
previous,
|
|
||||||
next,
|
|
||||||
};
|
|
||||||
|
|
||||||
// sourceToPermalink and permalinkToSidebar mapping.
|
|
||||||
const {source, permalink, version} = docsMetadataRaw[currentID];
|
|
||||||
sourceToPermalink[source] = permalink;
|
|
||||||
if (sidebar) {
|
|
||||||
permalinkToSidebar[permalink] = sidebar;
|
|
||||||
if (versioning.enabled && version) {
|
|
||||||
if (!versionToSidebars[version]) {
|
|
||||||
versionToSidebars[version] = new Set();
|
|
||||||
}
|
|
||||||
versionToSidebars[version].add(sidebar);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const convertDocLink = (item: SidebarItemDoc): SidebarItemLink => {
|
|
||||||
const docId = item.id;
|
|
||||||
const docMetadata = docsMetadataRaw[docId];
|
|
||||||
|
|
||||||
if (!docMetadata) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Bad sidebars file. The document id '${docId}' was used in the sidebar, but no document with this id could be found.
|
`Docs version ${
|
||||||
Available document ids=
|
versionMetadata.versionName
|
||||||
- ${Object.keys(docsMetadataRaw).sort().join('\n- ')}`,
|
} has no docs! At least one doc should exist at path=[${path.relative(
|
||||||
|
siteDir,
|
||||||
|
versionMetadata.docsDirPath,
|
||||||
|
)}]`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
async function processVersionDoc(docFile: DocFile) {
|
||||||
|
return processDocMetadata({
|
||||||
|
docFile,
|
||||||
|
versionMetadata,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.all(docFiles.map(processVersionDoc));
|
||||||
|
}
|
||||||
|
|
||||||
const {title, permalink, sidebar_label} = docMetadata;
|
async function loadVersion(
|
||||||
|
versionMetadata: VersionMetadata,
|
||||||
|
): Promise<LoadedVersion> {
|
||||||
|
const sidebars = loadSidebars(versionMetadata.sidebarFilePath);
|
||||||
|
const sidebarsUtils = createSidebarsUtils(sidebars);
|
||||||
|
|
||||||
|
const docsBase: DocMetadataBase[] = await loadVersionDocsBase(
|
||||||
|
versionMetadata,
|
||||||
|
);
|
||||||
|
const docsBaseById: Record<string, DocMetadataBase> = keyBy(
|
||||||
|
docsBase,
|
||||||
|
(doc) => doc.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
const validDocIds = Object.keys(docsBaseById);
|
||||||
|
sidebarsUtils.checkSidebarsDocIds(validDocIds);
|
||||||
|
|
||||||
|
// Add sidebar/next/previous to the docs
|
||||||
|
function addNavData(doc: DocMetadataBase): DocMetadata {
|
||||||
|
const {
|
||||||
|
sidebarName,
|
||||||
|
previousId,
|
||||||
|
nextId,
|
||||||
|
} = sidebarsUtils.getDocNavigation(doc.id);
|
||||||
|
const toDocNavLink = (navDocId: string): DocNavLink => ({
|
||||||
|
title: docsBaseById[navDocId].title,
|
||||||
|
permalink: docsBaseById[navDocId].permalink,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...doc,
|
||||||
|
sidebar: sidebarName,
|
||||||
|
previous: previousId ? toDocNavLink(previousId) : undefined,
|
||||||
|
next: nextId ? toDocNavLink(nextId) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs = docsBase.map(addNavData);
|
||||||
|
|
||||||
|
// sort to ensure consistent output for tests
|
||||||
|
docs.sort((a, b) => a.id.localeCompare(b.id));
|
||||||
|
|
||||||
|
// TODO annoying side effect!
|
||||||
|
Object.values(docs).forEach((loadedDoc) => {
|
||||||
|
const {source, permalink} = loadedDoc;
|
||||||
|
sourceToPermalink[source] = permalink;
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO really useful? replace with global state logic?
|
||||||
|
const permalinkToSidebar: PermalinkToSidebar = {};
|
||||||
|
Object.values(docs).forEach((doc) => {
|
||||||
|
if (doc.sidebar) {
|
||||||
|
permalinkToSidebar[doc.permalink] = doc.sidebar;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// The "main doc" is the "version entry point"
|
||||||
|
// We browse this doc by clicking on a version:
|
||||||
|
// - the "home" doc (at '/docs/')
|
||||||
|
// - the first doc of the first sidebar
|
||||||
|
// - a random doc (if no docs are in any sidebar... edge case)
|
||||||
|
function getMainDoc(): DocMetadata {
|
||||||
|
const versionHomeDoc = docs.find(
|
||||||
|
(doc) =>
|
||||||
|
doc.unversionedId === options.homePageId || doc.slug === '/',
|
||||||
|
);
|
||||||
|
const firstDocIdOfFirstSidebar = sidebarsUtils.getFirstDocIdOfFirstSidebar();
|
||||||
|
if (versionHomeDoc) {
|
||||||
|
return versionHomeDoc;
|
||||||
|
} else if (firstDocIdOfFirstSidebar) {
|
||||||
|
return docs.find((doc) => doc.id === firstDocIdOfFirstSidebar)!;
|
||||||
|
} else {
|
||||||
|
return docs[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'link',
|
...versionMetadata,
|
||||||
label: sidebar_label || title,
|
mainDocId: getMainDoc().unversionedId,
|
||||||
href: permalink,
|
sidebars,
|
||||||
|
permalinkToSidebar,
|
||||||
|
docs: docs.map(addNavData),
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
const normalizeItem = (item: SidebarItem): DocsSidebarItem => {
|
|
||||||
switch (item.type) {
|
|
||||||
case 'category':
|
|
||||||
return {...item, items: item.items.map(normalizeItem)};
|
|
||||||
case 'ref':
|
|
||||||
case 'doc':
|
|
||||||
return convertDocLink(item);
|
|
||||||
case 'link':
|
|
||||||
default:
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Transform the sidebar so that all sidebar item will be in the
|
|
||||||
// form of 'link' or 'category' only.
|
|
||||||
// This is what will be passed as props to the UI component.
|
|
||||||
const docsSidebars: DocsSidebar = Object.entries(loadedSidebars).reduce(
|
|
||||||
(acc: DocsSidebar, [sidebarId, sidebarItems]) => {
|
|
||||||
acc[sidebarId] = sidebarItems.map(normalizeItem);
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
docsMetadata,
|
loadedVersions: await Promise.all(versionsMetadata.map(loadVersion)),
|
||||||
docsDir,
|
|
||||||
docsSidebars,
|
|
||||||
permalinkToSidebar: objectWithKeySorted(permalinkToSidebar),
|
|
||||||
versionToSidebars,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
async contentLoaded({content, actions}) {
|
async contentLoaded({content, actions}) {
|
||||||
if (!content || Object.keys(content.docsMetadata).length === 0) {
|
const {loadedVersions} = content;
|
||||||
return;
|
const {docLayoutComponent, docItemComponent} = options;
|
||||||
}
|
|
||||||
|
|
||||||
const {docLayoutComponent, docItemComponent, routeBasePath} = options;
|
|
||||||
const {addRoute, createData, setGlobalData} = actions;
|
const {addRoute, createData, setGlobalData} = actions;
|
||||||
|
|
||||||
const pluginInstanceGlobalData: GlobalPluginData = {
|
const createDocRoutes = async (
|
||||||
path: normalizeUrl([baseUrl, options.routeBasePath]),
|
docs: DocMetadata[],
|
||||||
latestVersionName: versioning.latestVersion,
|
|
||||||
// Initialized empty, will be mutated
|
|
||||||
versions: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
setGlobalData<GlobalPluginData>(pluginInstanceGlobalData);
|
|
||||||
|
|
||||||
const createDocsBaseMetadata = (
|
|
||||||
version: DocsVersion,
|
|
||||||
): DocsBaseMetadata => {
|
|
||||||
const {docsSidebars, permalinkToSidebar, versionToSidebars} = content;
|
|
||||||
const neededSidebars: Set<string> =
|
|
||||||
versionToSidebars[version!] || new Set();
|
|
||||||
|
|
||||||
return {
|
|
||||||
docsSidebars: version
|
|
||||||
? pick(docsSidebars, Array.from(neededSidebars))
|
|
||||||
: docsSidebars,
|
|
||||||
permalinkToSidebar: version
|
|
||||||
? pickBy(permalinkToSidebar, (sidebar) =>
|
|
||||||
neededSidebars.has(sidebar),
|
|
||||||
)
|
|
||||||
: permalinkToSidebar,
|
|
||||||
version,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const genRoutes = async (
|
|
||||||
metadataItems: Metadata[],
|
|
||||||
): Promise<RouteConfig[]> => {
|
): Promise<RouteConfig[]> => {
|
||||||
const routes = await Promise.all(
|
const routes = await Promise.all(
|
||||||
metadataItems.map(async (metadataItem) => {
|
docs.map(async (metadataItem) => {
|
||||||
await createData(
|
await createData(
|
||||||
// Note that this created data path must be in sync with
|
// Note that this created data path must be in sync with
|
||||||
// metadataPath provided to mdx-loader.
|
// metadataPath provided to mdx-loader.
|
||||||
|
@ -411,111 +262,84 @@ Available document ids=
|
||||||
return routes.sort((a, b) => a.path.localeCompare(b.path));
|
return routes.sort((a, b) => a.path.localeCompare(b.path));
|
||||||
};
|
};
|
||||||
|
|
||||||
// We want latest version route to have lower priority
|
async function handleVersion(loadedVersion: LoadedVersion) {
|
||||||
// Otherwise `/docs/next/foo` would match
|
const versionMetadataPropPath = await createData(
|
||||||
// `/docs/:route` instead of `/docs/next/:route`.
|
`${docuHash(
|
||||||
const getVersionRoutePriority = (version: DocsVersion) =>
|
`version-${loadedVersion.versionName}-metadata-prop`,
|
||||||
version === versioning.latestVersion ? -1 : undefined;
|
)}.json`,
|
||||||
|
JSON.stringify(toVersionMetadataProp(loadedVersion), null, 2),
|
||||||
// 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 addVersionRoute = async (
|
|
||||||
docsBasePath: string,
|
|
||||||
docsBaseMetadata: DocsBaseMetadata,
|
|
||||||
docs: Metadata[],
|
|
||||||
priority?: number,
|
|
||||||
) => {
|
|
||||||
const docsBaseMetadataPath = await createData(
|
|
||||||
`${docuHash(normalizeUrl([docsBasePath, ':route']))}.json`,
|
|
||||||
JSON.stringify(docsBaseMetadata, null, 2),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const docsRoutes = await genRoutes(docs);
|
|
||||||
|
|
||||||
const mainDoc: Metadata =
|
|
||||||
docs.find(
|
|
||||||
(doc) =>
|
|
||||||
doc.unversionedId === options.homePageId || doc.slug === '/',
|
|
||||||
) ?? 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({
|
addRoute({
|
||||||
path: docsBasePath,
|
path: loadedVersion.versionPath,
|
||||||
exact: false, // allow matching /docs/* as well
|
// allow matching /docs/* as well
|
||||||
component: docLayoutComponent, // main docs component (DocPage)
|
exact: false,
|
||||||
routes: docsRoutes, // subroute for each doc
|
// main docs component (DocPage)
|
||||||
|
component: docLayoutComponent,
|
||||||
|
// sub-routes for each doc
|
||||||
|
routes: await createDocRoutes(loadedVersion.docs),
|
||||||
modules: {
|
modules: {
|
||||||
docsMetadata: aliasedSource(docsBaseMetadataPath),
|
versionMetadata: aliasedSource(versionMetadataPropPath),
|
||||||
},
|
},
|
||||||
priority,
|
priority: loadedVersion.routePriority,
|
||||||
});
|
});
|
||||||
};
|
|
||||||
// If versioning is enabled, we cleverly chunk the generated routes
|
|
||||||
// to be by version and pick only needed base metadata.
|
|
||||||
if (versioning.enabled) {
|
|
||||||
const docsMetadataByVersion = groupBy(
|
|
||||||
// sort to ensure consistent output for tests
|
|
||||||
Object.values(content.docsMetadata).sort((a, b) =>
|
|
||||||
a.id.localeCompare(b.id),
|
|
||||||
),
|
|
||||||
'version',
|
|
||||||
);
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
Object.keys(docsMetadataByVersion).map(async (version) => {
|
|
||||||
const docsMetadata = docsMetadataByVersion[version];
|
|
||||||
|
|
||||||
const isLatestVersion = version === versioning.latestVersion;
|
|
||||||
const docsBaseRoute = normalizeUrl([
|
|
||||||
baseUrl,
|
|
||||||
routeBasePath,
|
|
||||||
isLatestVersion ? '' : version,
|
|
||||||
]);
|
|
||||||
const docsBaseMetadata = createDocsBaseMetadata(version);
|
|
||||||
|
|
||||||
await addVersionRoute(
|
|
||||||
docsBaseRoute,
|
|
||||||
docsBaseMetadata,
|
|
||||||
docsMetadata,
|
|
||||||
getVersionRoutePriority(version),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const docsMetadata = Object.values(content.docsMetadata);
|
|
||||||
const docsBaseMetadata = createDocsBaseMetadata(null);
|
|
||||||
const docsBaseRoute = normalizeUrl([baseUrl, routeBasePath]);
|
|
||||||
await addVersionRoute(docsBaseRoute, docsBaseMetadata, docsMetadata);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure version ordering on the global data (latest first)
|
await Promise.all(loadedVersions.map(handleVersion));
|
||||||
pluginInstanceGlobalData.versions = sortBy(
|
|
||||||
pluginInstanceGlobalData.versions,
|
setGlobalData<GlobalPluginData>({
|
||||||
(versionMetadata: GlobalVersion) => {
|
path: normalizeUrl([baseUrl, options.routeBasePath]),
|
||||||
const orderedVersionNames = ['next', ...versions];
|
versions: loadedVersions.map(toGlobalDataVersion),
|
||||||
return orderedVersionNames.indexOf(versionMetadata.name!);
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
configureWebpack(_config, isServer, utils) {
|
configureWebpack(_config, isServer, utils) {
|
||||||
const {getBabelLoader, getCacheLoader} = utils;
|
const {getBabelLoader, getCacheLoader} = utils;
|
||||||
const {rehypePlugins, remarkPlugins} = options;
|
const {rehypePlugins, remarkPlugins} = options;
|
||||||
|
|
||||||
|
const docsMarkdownOptions: DocsMarkdownOption = {
|
||||||
|
siteDir,
|
||||||
|
sourceToPermalink,
|
||||||
|
versionsMetadata,
|
||||||
|
onBrokenMarkdownLink: (brokenMarkdownLink) => {
|
||||||
|
// TODO make this warning configurable?
|
||||||
|
console.warn(
|
||||||
|
chalk.yellow(
|
||||||
|
`Docs markdown link couldn't be resolved: (${brokenMarkdownLink.link}) in ${brokenMarkdownLink.filePath} for version ${brokenMarkdownLink.version.versionName}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createMDXLoaderRule(): RuleSetRule {
|
||||||
|
return {
|
||||||
|
test: /(\.mdx?)$/,
|
||||||
|
include: versionsMetadata.map((vmd) => vmd.docsDirPath),
|
||||||
|
use: compact([
|
||||||
|
getCacheLoader(isServer),
|
||||||
|
getBabelLoader(isServer),
|
||||||
|
{
|
||||||
|
loader: require.resolve('@docusaurus/mdx-loader'),
|
||||||
|
options: {
|
||||||
|
remarkPlugins,
|
||||||
|
rehypePlugins,
|
||||||
|
staticDir: path.join(siteDir, STATIC_DIR_NAME),
|
||||||
|
metadataPath: (mdxPath: string) => {
|
||||||
|
// Note that metadataPath must be the same/in-sync as
|
||||||
|
// the path from createData for each MDX.
|
||||||
|
const aliasedPath = aliasedSitePath(mdxPath, siteDir);
|
||||||
|
return path.join(dataDir, `${docuHash(aliasedPath)}.json`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: path.resolve(__dirname, './markdown/index.js'),
|
||||||
|
options: docsMarkdownOptions,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Suppress warnings about non-existing of versions file.
|
// Suppress warnings about non-existing of versions file.
|
||||||
const stats = {
|
const stats = {
|
||||||
warningsFilter: [VERSIONS_JSON_FILE],
|
warningsFilter: [VERSIONS_JSON_FILE],
|
||||||
|
@ -532,55 +356,11 @@ Available document ids=
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [createMDXLoaderRule()],
|
||||||
{
|
|
||||||
test: /(\.mdx?)$/,
|
|
||||||
include: [docsDir, versionedDir].filter(Boolean),
|
|
||||||
use: [
|
|
||||||
getCacheLoader(isServer),
|
|
||||||
getBabelLoader(isServer),
|
|
||||||
{
|
|
||||||
loader: require.resolve('@docusaurus/mdx-loader'),
|
|
||||||
options: {
|
|
||||||
remarkPlugins,
|
|
||||||
rehypePlugins,
|
|
||||||
staticDir: path.join(siteDir, STATIC_DIR_NAME),
|
|
||||||
metadataPath: (mdxPath: string) => {
|
|
||||||
// Note that metadataPath must be the same/in-sync as
|
|
||||||
// the path from createData for each MDX.
|
|
||||||
const aliasedPath = aliasedSitePath(mdxPath, siteDir);
|
|
||||||
return path.join(
|
|
||||||
dataDir,
|
|
||||||
`${docuHash(aliasedPath)}.json`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
loader: path.resolve(__dirname, './markdown/index.js'),
|
|
||||||
options: {
|
|
||||||
siteDir,
|
|
||||||
docsDir,
|
|
||||||
sourceToPermalink,
|
|
||||||
versionedDir,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
].filter(Boolean),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
} as Configuration;
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateOptions({
|
export {validateOptions} from './options';
|
||||||
validate,
|
|
||||||
options,
|
|
||||||
}: OptionValidationContext<PluginOptions, ValidationError>): ValidationResult<
|
|
||||||
PluginOptions,
|
|
||||||
ValidationError
|
|
||||||
> {
|
|
||||||
const validatedOptions = validate(PluginOptionSchema, options);
|
|
||||||
return validatedOptions;
|
|
||||||
}
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ const GIT_COMMIT_TIMESTAMP_AUTHOR_REGEX = /^(\d+), (.+)$/;
|
||||||
|
|
||||||
let showedGitRequirementError = false;
|
let showedGitRequirementError = false;
|
||||||
|
|
||||||
export default async function getFileLastUpdate(
|
export async function getFileLastUpdate(
|
||||||
filePath?: string,
|
filePath?: string,
|
||||||
): Promise<FileLastUpdateData | null> {
|
): Promise<FileLastUpdateData | null> {
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
### Not Existing Docs
|
||||||
|
|
||||||
|
- [docNotExist1](docNotExist1.md)
|
||||||
|
- [docNotExist2](./docNotExist2.mdx)
|
||||||
|
- [docNotExist3](../docNotExist3.mdx)
|
||||||
|
- [docNotExist4](./subdir/docNotExist4.md)
|
|
@ -7,13 +7,41 @@
|
||||||
|
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import linkify from '../linkify';
|
import {linkify} from '../linkify';
|
||||||
import {SourceToPermalink} from '../../types';
|
import {
|
||||||
import {VERSIONED_DOCS_DIR} from '../../constants';
|
DocsMarkdownOption,
|
||||||
|
SourceToPermalink,
|
||||||
|
VersionMetadata,
|
||||||
|
BrokenMarkdownLink,
|
||||||
|
} from '../../types';
|
||||||
|
import {VERSIONED_DOCS_DIR, CURRENT_VERSION_NAME} from '../../constants';
|
||||||
|
|
||||||
|
function createFakeVersion(
|
||||||
|
versionName: string,
|
||||||
|
docsDirPath: string,
|
||||||
|
): VersionMetadata {
|
||||||
|
return {
|
||||||
|
versionName,
|
||||||
|
versionLabel: 'Any',
|
||||||
|
versionPath: 'any',
|
||||||
|
docsDirPath,
|
||||||
|
sidebarFilePath: 'any',
|
||||||
|
routePriority: undefined,
|
||||||
|
isLast: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const siteDir = path.join(__dirname, '__fixtures__');
|
const siteDir = path.join(__dirname, '__fixtures__');
|
||||||
const docsDir = path.join(siteDir, 'docs');
|
|
||||||
const versionedDir = path.join(siteDir, VERSIONED_DOCS_DIR);
|
const versionCurrent = createFakeVersion(
|
||||||
|
CURRENT_VERSION_NAME,
|
||||||
|
path.join(siteDir, 'docs'),
|
||||||
|
);
|
||||||
|
const version100 = createFakeVersion(
|
||||||
|
CURRENT_VERSION_NAME,
|
||||||
|
path.join(siteDir, VERSIONED_DOCS_DIR, 'version-1.0.0'),
|
||||||
|
);
|
||||||
|
|
||||||
const sourceToPermalink: SourceToPermalink = {
|
const sourceToPermalink: SourceToPermalink = {
|
||||||
'@site/docs/doc1.md': '/docs/doc1',
|
'@site/docs/doc1.md': '/docs/doc1',
|
||||||
'@site/docs/doc2.md': '/docs/doc2',
|
'@site/docs/doc2.md': '/docs/doc2',
|
||||||
|
@ -24,28 +52,34 @@ const sourceToPermalink: SourceToPermalink = {
|
||||||
'/docs/1.0.0/subdir/doc1',
|
'/docs/1.0.0/subdir/doc1',
|
||||||
};
|
};
|
||||||
|
|
||||||
const transform = (filepath) => {
|
function createMarkdownOptions(
|
||||||
const content = fs.readFileSync(filepath, 'utf-8');
|
options?: Partial<DocsMarkdownOption>,
|
||||||
const transformedContent = linkify(
|
): DocsMarkdownOption {
|
||||||
content,
|
return {
|
||||||
filepath,
|
|
||||||
docsDir,
|
|
||||||
siteDir,
|
|
||||||
sourceToPermalink,
|
sourceToPermalink,
|
||||||
versionedDir,
|
onBrokenMarkdownLink: () => {},
|
||||||
);
|
versionsMetadata: [versionCurrent, version100],
|
||||||
|
siteDir,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const transform = (filepath: string, options?: Partial<DocsMarkdownOption>) => {
|
||||||
|
const markdownOptions = createMarkdownOptions(options);
|
||||||
|
const content = fs.readFileSync(filepath, 'utf-8');
|
||||||
|
const transformedContent = linkify(content, filepath, markdownOptions);
|
||||||
return [content, transformedContent];
|
return [content, transformedContent];
|
||||||
};
|
};
|
||||||
|
|
||||||
test('transform nothing', () => {
|
test('transform nothing', () => {
|
||||||
const doc1 = path.join(docsDir, 'doc1.md');
|
const doc1 = path.join(versionCurrent.docsDirPath, 'doc1.md');
|
||||||
const [content, transformedContent] = transform(doc1);
|
const [content, transformedContent] = transform(doc1);
|
||||||
expect(transformedContent).toMatchSnapshot();
|
expect(transformedContent).toMatchSnapshot();
|
||||||
expect(content).toEqual(transformedContent);
|
expect(content).toEqual(transformedContent);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('transform to correct links', () => {
|
test('transform to correct links', () => {
|
||||||
const doc2 = path.join(docsDir, 'doc2.md');
|
const doc2 = path.join(versionCurrent.docsDirPath, 'doc2.md');
|
||||||
const [content, transformedContent] = transform(doc2);
|
const [content, transformedContent] = transform(doc2);
|
||||||
expect(transformedContent).toMatchSnapshot();
|
expect(transformedContent).toMatchSnapshot();
|
||||||
expect(transformedContent).toContain('](/docs/doc1');
|
expect(transformedContent).toContain('](/docs/doc1');
|
||||||
|
@ -58,7 +92,8 @@ test('transform to correct links', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('transform relative links', () => {
|
test('transform relative links', () => {
|
||||||
const doc3 = path.join(docsDir, 'subdir', 'doc3.md');
|
const doc3 = path.join(versionCurrent.docsDirPath, 'subdir', 'doc3.md');
|
||||||
|
|
||||||
const [content, transformedContent] = transform(doc3);
|
const [content, transformedContent] = transform(doc3);
|
||||||
expect(transformedContent).toMatchSnapshot();
|
expect(transformedContent).toMatchSnapshot();
|
||||||
expect(transformedContent).toContain('](/docs/doc2');
|
expect(transformedContent).toContain('](/docs/doc2');
|
||||||
|
@ -67,7 +102,7 @@ test('transform relative links', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('transforms reference links', () => {
|
test('transforms reference links', () => {
|
||||||
const doc4 = path.join(docsDir, 'doc4.md');
|
const doc4 = path.join(versionCurrent.docsDirPath, 'doc4.md');
|
||||||
const [content, transformedContent] = transform(doc4);
|
const [content, transformedContent] = transform(doc4);
|
||||||
expect(transformedContent).toMatchSnapshot();
|
expect(transformedContent).toMatchSnapshot();
|
||||||
expect(transformedContent).toContain('[doc1]: /docs/doc1');
|
expect(transformedContent).toContain('[doc1]: /docs/doc1');
|
||||||
|
@ -77,8 +112,38 @@ test('transforms reference links', () => {
|
||||||
expect(content).not.toEqual(transformedContent);
|
expect(content).not.toEqual(transformedContent);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('report broken markdown links', () => {
|
||||||
|
const doc5 = path.join(versionCurrent.docsDirPath, 'doc5.md');
|
||||||
|
const onBrokenMarkdownLink = jest.fn();
|
||||||
|
const [content, transformedContent] = transform(doc5, {
|
||||||
|
onBrokenMarkdownLink,
|
||||||
|
});
|
||||||
|
expect(transformedContent).toEqual(content);
|
||||||
|
expect(onBrokenMarkdownLink).toHaveBeenCalledTimes(4);
|
||||||
|
expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(1, {
|
||||||
|
filePath: doc5,
|
||||||
|
link: 'docNotExist1.md',
|
||||||
|
version: versionCurrent,
|
||||||
|
} as BrokenMarkdownLink);
|
||||||
|
expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(2, {
|
||||||
|
filePath: doc5,
|
||||||
|
link: './docNotExist2.mdx',
|
||||||
|
version: versionCurrent,
|
||||||
|
} as BrokenMarkdownLink);
|
||||||
|
expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(3, {
|
||||||
|
filePath: doc5,
|
||||||
|
link: '../docNotExist3.mdx',
|
||||||
|
version: versionCurrent,
|
||||||
|
} as BrokenMarkdownLink);
|
||||||
|
expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(4, {
|
||||||
|
filePath: doc5,
|
||||||
|
link: './subdir/docNotExist4.md',
|
||||||
|
version: versionCurrent,
|
||||||
|
} as BrokenMarkdownLink);
|
||||||
|
});
|
||||||
|
|
||||||
test('transforms absolute links in versioned docs', () => {
|
test('transforms absolute links in versioned docs', () => {
|
||||||
const doc2 = path.join(versionedDir, 'version-1.0.0', 'doc2.md');
|
const doc2 = path.join(version100.docsDirPath, 'doc2.md');
|
||||||
const [content, transformedContent] = transform(doc2);
|
const [content, transformedContent] = transform(doc2);
|
||||||
expect(transformedContent).toMatchSnapshot();
|
expect(transformedContent).toMatchSnapshot();
|
||||||
expect(transformedContent).toContain('](/docs/1.0.0/subdir/doc1');
|
expect(transformedContent).toContain('](/docs/1.0.0/subdir/doc1');
|
||||||
|
@ -89,7 +154,7 @@ test('transforms absolute links in versioned docs', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('transforms relative links in versioned docs', () => {
|
test('transforms relative links in versioned docs', () => {
|
||||||
const doc1 = path.join(versionedDir, 'version-1.0.0', 'subdir', 'doc1.md');
|
const doc1 = path.join(version100.docsDirPath, 'subdir', 'doc1.md');
|
||||||
const [content, transformedContent] = transform(doc1);
|
const [content, transformedContent] = transform(doc1);
|
||||||
expect(transformedContent).toMatchSnapshot();
|
expect(transformedContent).toMatchSnapshot();
|
||||||
expect(transformedContent).toContain('](/docs/1.0.0/doc2');
|
expect(transformedContent).toContain('](/docs/1.0.0/doc2');
|
||||||
|
|
|
@ -7,26 +7,15 @@
|
||||||
|
|
||||||
import {getOptions} from 'loader-utils';
|
import {getOptions} from 'loader-utils';
|
||||||
import {loader} from 'webpack';
|
import {loader} from 'webpack';
|
||||||
import linkify from './linkify';
|
import {linkify} from './linkify';
|
||||||
|
import {DocsMarkdownOption} from '../types';
|
||||||
|
|
||||||
const markdownLoader: loader.Loader = function (source) {
|
const markdownLoader: loader.Loader = function (source) {
|
||||||
const fileString = source as string;
|
const fileString = source as string;
|
||||||
const callback = this.async();
|
const callback = this.async();
|
||||||
const {docsDir, siteDir, versionedDir, sourceToPermalink} = getOptions(this);
|
const options = getOptions(this) as DocsMarkdownOption;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
callback &&
|
callback && callback(null, linkify(fileString, this.resourcePath, options))
|
||||||
callback(
|
|
||||||
null,
|
|
||||||
linkify(
|
|
||||||
fileString,
|
|
||||||
this.resourcePath,
|
|
||||||
docsDir,
|
|
||||||
siteDir,
|
|
||||||
sourceToPermalink,
|
|
||||||
versionedDir,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,68 +7,81 @@
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {resolve} from 'url';
|
import {resolve} from 'url';
|
||||||
import {getSubFolder} from '@docusaurus/utils';
|
import {
|
||||||
import {SourceToPermalink} from '../types';
|
DocsMarkdownOption,
|
||||||
|
VersionMetadata,
|
||||||
|
BrokenMarkdownLink,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
export default function (
|
function getVersion(filePath: string, options: DocsMarkdownOption) {
|
||||||
|
const versionFound = options.versionsMetadata.find((version) =>
|
||||||
|
filePath.startsWith(version.docsDirPath),
|
||||||
|
);
|
||||||
|
if (!versionFound) {
|
||||||
|
throw new Error(
|
||||||
|
`Unexpected, markdown file does not belong to any docs version! file=${filePath}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return versionFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceMarkdownLinks(
|
||||||
fileString: string,
|
fileString: string,
|
||||||
filePath: string,
|
filePath: string,
|
||||||
docsDir: string,
|
version: VersionMetadata,
|
||||||
siteDir: string,
|
options: DocsMarkdownOption,
|
||||||
sourceToPermalink: SourceToPermalink,
|
) {
|
||||||
versionedDir?: string,
|
const {siteDir, sourceToPermalink, onBrokenMarkdownLink} = options;
|
||||||
): string {
|
const {docsDirPath} = version;
|
||||||
// Determine the source dir. e.g: /website/docs, /website/versioned_docs/version-1.0.0
|
|
||||||
let sourceDir: string | undefined;
|
|
||||||
const thisSource = filePath;
|
|
||||||
if (thisSource.startsWith(docsDir)) {
|
|
||||||
sourceDir = docsDir;
|
|
||||||
} else if (versionedDir && thisSource.startsWith(versionedDir)) {
|
|
||||||
const specificVersionDir = getSubFolder(thisSource, versionedDir);
|
|
||||||
// e.g: specificVersionDir = version-1.0.0
|
|
||||||
if (specificVersionDir) {
|
|
||||||
sourceDir = path.join(versionedDir, specificVersionDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = fileString;
|
|
||||||
|
|
||||||
// Replace internal markdown linking (except in fenced blocks).
|
// Replace internal markdown linking (except in fenced blocks).
|
||||||
if (sourceDir) {
|
let fencedBlock = false;
|
||||||
let fencedBlock = false;
|
const lines = fileString.split('\n').map((line) => {
|
||||||
const lines = content.split('\n').map((line) => {
|
if (line.trim().startsWith('```')) {
|
||||||
if (line.trim().startsWith('```')) {
|
fencedBlock = !fencedBlock;
|
||||||
fencedBlock = !fencedBlock;
|
}
|
||||||
}
|
if (fencedBlock) {
|
||||||
if (fencedBlock) {
|
return line;
|
||||||
return line;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let modifiedLine = line;
|
let modifiedLine = line;
|
||||||
// Replace inline-style links or reference-style links e.g:
|
// Replace inline-style links or reference-style links e.g:
|
||||||
// This is [Document 1](doc1.md) -> we replace this doc1.md with correct link
|
// This is [Document 1](doc1.md) -> we replace this doc1.md with correct link
|
||||||
// [doc1]: doc1.md -> we replace this doc1.md with correct link
|
// [doc1]: doc1.md -> we replace this doc1.md with correct link
|
||||||
const mdRegex = /(?:(?:\]\()|(?:\]:\s?))(?!https)([^'")\]\s>]+\.mdx?)/g;
|
const mdRegex = /(?:(?:\]\()|(?:\]:\s?))(?!https)([^'")\]\s>]+\.mdx?)/g;
|
||||||
let mdMatch = mdRegex.exec(modifiedLine);
|
let mdMatch = mdRegex.exec(modifiedLine);
|
||||||
while (mdMatch !== null) {
|
while (mdMatch !== null) {
|
||||||
// Replace it to correct html link.
|
// Replace it to correct html link.
|
||||||
const mdLink = mdMatch[1];
|
const mdLink = mdMatch[1];
|
||||||
const targetSource = `${sourceDir}/${mdLink}`;
|
const targetSource = `${docsDirPath}/${mdLink}`;
|
||||||
const aliasedSource = (source: string) =>
|
const aliasedSource = (source: string) =>
|
||||||
`@site/${path.relative(siteDir, source)}`;
|
`@site/${path.relative(siteDir, source)}`;
|
||||||
const permalink =
|
const permalink =
|
||||||
sourceToPermalink[aliasedSource(resolve(thisSource, mdLink))] ||
|
sourceToPermalink[aliasedSource(resolve(filePath, mdLink))] ||
|
||||||
sourceToPermalink[aliasedSource(targetSource)];
|
sourceToPermalink[aliasedSource(targetSource)];
|
||||||
if (permalink) {
|
if (permalink) {
|
||||||
modifiedLine = modifiedLine.replace(mdLink, permalink);
|
modifiedLine = modifiedLine.replace(mdLink, permalink);
|
||||||
}
|
} else {
|
||||||
mdMatch = mdRegex.exec(modifiedLine);
|
const brokenMarkdownLink: BrokenMarkdownLink = {
|
||||||
|
version,
|
||||||
|
filePath,
|
||||||
|
link: mdLink,
|
||||||
|
};
|
||||||
|
onBrokenMarkdownLink(brokenMarkdownLink);
|
||||||
}
|
}
|
||||||
return modifiedLine;
|
mdMatch = mdRegex.exec(modifiedLine);
|
||||||
});
|
}
|
||||||
|
return modifiedLine;
|
||||||
|
});
|
||||||
|
|
||||||
content = lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
return content;
|
export function linkify(
|
||||||
|
fileString: string,
|
||||||
|
filePath: string,
|
||||||
|
options: DocsMarkdownOption,
|
||||||
|
): string {
|
||||||
|
const version = getVersion(filePath, options);
|
||||||
|
return replaceMarkdownLinks(fileString, filePath, version, options);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,188 +0,0 @@
|
||||||
/**
|
|
||||||
* 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 path from 'path';
|
|
||||||
import {
|
|
||||||
parseMarkdownFile,
|
|
||||||
aliasedSitePath,
|
|
||||||
normalizeUrl,
|
|
||||||
getEditUrl,
|
|
||||||
} from '@docusaurus/utils';
|
|
||||||
import {LoadContext} from '@docusaurus/types';
|
|
||||||
|
|
||||||
import lastUpdate from './lastUpdate';
|
|
||||||
import {
|
|
||||||
MetadataRaw,
|
|
||||||
LastUpdateData,
|
|
||||||
MetadataOptions,
|
|
||||||
Env,
|
|
||||||
VersioningEnv,
|
|
||||||
} from './types';
|
|
||||||
import getSlug from './slug';
|
|
||||||
import {escapeRegExp} from 'lodash';
|
|
||||||
|
|
||||||
function removeVersionPrefix(str: string, version: string): string {
|
|
||||||
return str.replace(new RegExp(`^version-${escapeRegExp(version)}/?`), '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function inferVersion(
|
|
||||||
dirName: string,
|
|
||||||
versioning: VersioningEnv,
|
|
||||||
): string | undefined {
|
|
||||||
if (!versioning.enabled) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if (/^version-/.test(dirName)) {
|
|
||||||
const inferredVersion = dirName
|
|
||||||
.split('/', 1)
|
|
||||||
.shift()!
|
|
||||||
.replace(/^version-/, '');
|
|
||||||
if (inferredVersion && versioning.versions.includes(inferredVersion)) {
|
|
||||||
return inferredVersion;
|
|
||||||
}
|
|
||||||
throw new Error(
|
|
||||||
`Can't infer version from folder=${dirName}
|
|
||||||
Expected versions:
|
|
||||||
- ${versioning.versions.join('- ')}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return 'next';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Args = {
|
|
||||||
source: string;
|
|
||||||
refDir: string;
|
|
||||||
context: LoadContext;
|
|
||||||
options: MetadataOptions;
|
|
||||||
env: Env;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function lastUpdated(
|
|
||||||
filePath: string,
|
|
||||||
options: MetadataOptions,
|
|
||||||
): Promise<LastUpdateData> {
|
|
||||||
const {showLastUpdateAuthor, showLastUpdateTime} = options;
|
|
||||||
if (showLastUpdateAuthor || showLastUpdateTime) {
|
|
||||||
// Use fake data in dev for faster development.
|
|
||||||
const fileLastUpdateData =
|
|
||||||
process.env.NODE_ENV === 'production'
|
|
||||||
? await lastUpdate(filePath)
|
|
||||||
: {
|
|
||||||
author: 'Author',
|
|
||||||
timestamp: 1539502055,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (fileLastUpdateData) {
|
|
||||||
const {author, timestamp} = fileLastUpdateData;
|
|
||||||
return {
|
|
||||||
lastUpdatedAt: showLastUpdateTime ? timestamp : undefined,
|
|
||||||
lastUpdatedBy: showLastUpdateAuthor ? author : undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function processMetadata({
|
|
||||||
source,
|
|
||||||
refDir,
|
|
||||||
context,
|
|
||||||
options,
|
|
||||||
env,
|
|
||||||
}: Args): Promise<MetadataRaw> {
|
|
||||||
const {routeBasePath, editUrl, homePageId} = options;
|
|
||||||
const {siteDir, baseUrl} = context;
|
|
||||||
const {versioning} = env;
|
|
||||||
const filePath = path.join(refDir, source);
|
|
||||||
|
|
||||||
const fileMarkdownPromise = parseMarkdownFile(filePath);
|
|
||||||
const lastUpdatedPromise = lastUpdated(filePath, options);
|
|
||||||
|
|
||||||
const dirNameWithVersion = path.dirname(source); // ex: version-1.0.0/foo
|
|
||||||
const version = inferVersion(dirNameWithVersion, versioning); // ex: 1.0.0
|
|
||||||
const dirNameWithoutVersion = // ex: foo
|
|
||||||
version && version !== 'next'
|
|
||||||
? removeVersionPrefix(dirNameWithVersion, version)
|
|
||||||
: dirNameWithVersion;
|
|
||||||
|
|
||||||
// The version portion of the url path. Eg: 'next', '1.0.0', and ''.
|
|
||||||
const versionPath =
|
|
||||||
version && version !== versioning.latestVersion ? version : '';
|
|
||||||
|
|
||||||
const relativePath = path.relative(siteDir, filePath);
|
|
||||||
|
|
||||||
const docsEditUrl = getEditUrl(relativePath, editUrl);
|
|
||||||
|
|
||||||
const {frontMatter = {}, excerpt} = await fileMarkdownPromise;
|
|
||||||
const {sidebar_label, custom_edit_url} = frontMatter;
|
|
||||||
|
|
||||||
// Default base id is the file name.
|
|
||||||
const baseID: string =
|
|
||||||
frontMatter.id || path.basename(source, path.extname(source));
|
|
||||||
if (baseID.includes('/')) {
|
|
||||||
throw new Error('Document id cannot include "/".');
|
|
||||||
}
|
|
||||||
|
|
||||||
// test for website/docs folder, not a versioned folder
|
|
||||||
// TODO legacy test, looks bad
|
|
||||||
const isCurrrentDocs = dirNameWithVersion === '.';
|
|
||||||
const id = isCurrrentDocs ? baseID : `${dirNameWithVersion}/${baseID}`;
|
|
||||||
const unversionedId = version ? removeVersionPrefix(id, version) : id;
|
|
||||||
|
|
||||||
const isDocsHomePage = unversionedId === (homePageId ?? '_index');
|
|
||||||
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`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const docSlug = isDocsHomePage
|
|
||||||
? '/'
|
|
||||||
: getSlug({
|
|
||||||
baseID,
|
|
||||||
dirName: dirNameWithoutVersion,
|
|
||||||
frontmatterSlug: frontMatter.slug,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Default title is the id.
|
|
||||||
const title: string = frontMatter.title || baseID;
|
|
||||||
|
|
||||||
const description: string = frontMatter.description || excerpt;
|
|
||||||
|
|
||||||
const permalink = normalizeUrl([
|
|
||||||
baseUrl,
|
|
||||||
routeBasePath,
|
|
||||||
versionPath,
|
|
||||||
docSlug,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const {lastUpdatedAt, lastUpdatedBy} = await lastUpdatedPromise;
|
|
||||||
|
|
||||||
// Assign all of object properties during instantiation (if possible) for
|
|
||||||
// NodeJS optimization.
|
|
||||||
// Adding properties to object after instantiation will cause hidden
|
|
||||||
// class transitions.
|
|
||||||
const metadata: MetadataRaw = {
|
|
||||||
unversionedId,
|
|
||||||
id,
|
|
||||||
isDocsHomePage,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
source: aliasedSitePath(filePath, siteDir),
|
|
||||||
slug: docSlug,
|
|
||||||
permalink,
|
|
||||||
editUrl: custom_edit_url !== undefined ? custom_edit_url : docsEditUrl,
|
|
||||||
version,
|
|
||||||
lastUpdatedBy,
|
|
||||||
lastUpdatedAt,
|
|
||||||
sidebar_label,
|
|
||||||
};
|
|
||||||
|
|
||||||
return metadata;
|
|
||||||
}
|
|
|
@ -12,13 +12,17 @@ import {
|
||||||
AdmonitionsSchema,
|
AdmonitionsSchema,
|
||||||
URISchema,
|
URISchema,
|
||||||
} from '@docusaurus/utils-validation';
|
} from '@docusaurus/utils-validation';
|
||||||
|
import {OptionValidationContext, ValidationResult} from '@docusaurus/types';
|
||||||
|
import {ValidationError} from '@hapi/joi';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import admonitions from 'remark-admonitions';
|
||||||
|
|
||||||
export const DEFAULT_OPTIONS: PluginOptions = {
|
export const DEFAULT_OPTIONS: Omit<PluginOptions, 'id'> = {
|
||||||
path: 'docs', // Path to data on filesystem, relative to site dir.
|
path: 'docs', // Path to data on filesystem, relative to site dir.
|
||||||
routeBasePath: 'docs', // URL Route.
|
routeBasePath: 'docs', // URL Route.
|
||||||
homePageId: undefined, // TODO remove soon, deprecated
|
homePageId: undefined, // TODO remove soon, deprecated
|
||||||
include: ['**/*.{md,mdx}'], // Extensions to include.
|
include: ['**/*.{md,mdx}'], // Extensions to include.
|
||||||
sidebarPath: '', // Path to sidebar configuration for showing a list of markdown pages.
|
sidebarPath: 'sidebars.json', // Path to sidebar configuration for showing a list of markdown pages.
|
||||||
docLayoutComponent: '@theme/DocPage',
|
docLayoutComponent: '@theme/DocPage',
|
||||||
docItemComponent: '@theme/DocItem',
|
docItemComponent: '@theme/DocItem',
|
||||||
remarkPlugins: [],
|
remarkPlugins: [],
|
||||||
|
@ -27,10 +31,11 @@ export const DEFAULT_OPTIONS: PluginOptions = {
|
||||||
showLastUpdateAuthor: false,
|
showLastUpdateAuthor: false,
|
||||||
admonitions: {},
|
admonitions: {},
|
||||||
excludeNextVersionDocs: false,
|
excludeNextVersionDocs: false,
|
||||||
|
includeCurrentVersion: true,
|
||||||
disableVersioning: false,
|
disableVersioning: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PluginOptionSchema = Joi.object({
|
export const OptionsSchema = Joi.object({
|
||||||
path: Joi.string().default(DEFAULT_OPTIONS.path),
|
path: Joi.string().default(DEFAULT_OPTIONS.path),
|
||||||
editUrl: URISchema,
|
editUrl: URISchema,
|
||||||
routeBasePath: Joi.string().allow('').default(DEFAULT_OPTIONS.routeBasePath),
|
routeBasePath: Joi.string().allow('').default(DEFAULT_OPTIONS.routeBasePath),
|
||||||
|
@ -49,5 +54,50 @@ export const PluginOptionSchema = Joi.object({
|
||||||
excludeNextVersionDocs: Joi.bool().default(
|
excludeNextVersionDocs: Joi.bool().default(
|
||||||
DEFAULT_OPTIONS.excludeNextVersionDocs,
|
DEFAULT_OPTIONS.excludeNextVersionDocs,
|
||||||
),
|
),
|
||||||
|
includeCurrentVersion: Joi.bool().default(
|
||||||
|
DEFAULT_OPTIONS.includeCurrentVersion,
|
||||||
|
),
|
||||||
disableVersioning: Joi.bool().default(DEFAULT_OPTIONS.disableVersioning),
|
disableVersioning: Joi.bool().default(DEFAULT_OPTIONS.disableVersioning),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO bad validation function types
|
||||||
|
export function validateOptions({
|
||||||
|
validate,
|
||||||
|
options,
|
||||||
|
}: OptionValidationContext<PluginOptions, ValidationError>): ValidationResult<
|
||||||
|
PluginOptions,
|
||||||
|
ValidationError
|
||||||
|
> {
|
||||||
|
// TODO remove homePageId before end of 2020
|
||||||
|
// "slug: /" is better because the home doc can be different across versions
|
||||||
|
if (options.homePageId) {
|
||||||
|
console.log(
|
||||||
|
chalk.red(
|
||||||
|
`The docs plugin option homePageId=${options.homePageId} is deprecated. To make a doc the "home", prefer frontmatter: "slug: /"`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof options.excludeNextVersionDocs !== 'undefined') {
|
||||||
|
console.log(
|
||||||
|
chalk.red(
|
||||||
|
`The docs plugin option excludeNextVersionDocs=${
|
||||||
|
options.excludeNextVersionDocs
|
||||||
|
} is deprecated. Use the includeCurrentVersion=${!options.excludeNextVersionDocs} option instead!"`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
options.includeCurrentVersion = !options.excludeNextVersionDocs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error: TODO bad OptionValidationContext, need refactor
|
||||||
|
const normalizedOptions: PluginOptions = validate(OptionsSchema, options);
|
||||||
|
|
||||||
|
if (normalizedOptions.admonitions) {
|
||||||
|
normalizedOptions.remarkPlugins = normalizedOptions.remarkPlugins.concat([
|
||||||
|
[admonitions, normalizedOptions.admonitions],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error: TODO bad OptionValidationContext, need refactor
|
||||||
|
return normalizedOptions;
|
||||||
|
}
|
|
@ -1,63 +0,0 @@
|
||||||
/**
|
|
||||||
* 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 {Sidebar, SidebarItem, Order} from './types';
|
|
||||||
|
|
||||||
// Build the docs meta such as next, previous, category and sidebar.
|
|
||||||
export default function createOrder(allSidebars: Sidebar = {}): Order {
|
|
||||||
const order: Order = {};
|
|
||||||
|
|
||||||
Object.keys(allSidebars).forEach((sidebarId) => {
|
|
||||||
const sidebar = allSidebars[sidebarId];
|
|
||||||
|
|
||||||
const ids: string[] = [];
|
|
||||||
const indexItems = ({items}: {items: SidebarItem[]}) => {
|
|
||||||
items.forEach((item) => {
|
|
||||||
switch (item.type) {
|
|
||||||
case 'category':
|
|
||||||
indexItems({
|
|
||||||
items: item.items,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'ref':
|
|
||||||
case 'link':
|
|
||||||
// Refs and links should not be shown in navigation.
|
|
||||||
break;
|
|
||||||
case 'doc':
|
|
||||||
ids.push(item.id);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
indexItems({items: sidebar});
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
for (let i = 0; i < ids.length; i++) {
|
|
||||||
const id = ids[i];
|
|
||||||
let previous;
|
|
||||||
let next;
|
|
||||||
|
|
||||||
if (i > 0) {
|
|
||||||
previous = ids[i - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i < ids.length - 1) {
|
|
||||||
next = ids[i + 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
order[id] = {
|
|
||||||
previous,
|
|
||||||
next,
|
|
||||||
sidebar: sidebarId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return order;
|
|
||||||
}
|
|
70
packages/docusaurus-plugin-content-docs/src/props.ts
Normal file
70
packages/docusaurus-plugin-content-docs/src/props.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
LoadedVersion,
|
||||||
|
PropSidebars,
|
||||||
|
SidebarItemDoc,
|
||||||
|
SidebarItemLink,
|
||||||
|
PropVersionMetadata,
|
||||||
|
SidebarItem,
|
||||||
|
PropSidebarItem,
|
||||||
|
} from './types';
|
||||||
|
import {keyBy, mapValues} from 'lodash';
|
||||||
|
|
||||||
|
export function toSidebarsProp(loadedVersion: LoadedVersion): PropSidebars {
|
||||||
|
const docsById = keyBy(loadedVersion.docs, (doc) => doc.id);
|
||||||
|
|
||||||
|
const convertDocLink = (item: SidebarItemDoc): SidebarItemLink => {
|
||||||
|
const docId = item.id;
|
||||||
|
const docMetadata = docsById[docId];
|
||||||
|
|
||||||
|
if (!docMetadata) {
|
||||||
|
throw new Error(
|
||||||
|
`Bad sidebars file. The document id '${docId}' was used in the sidebar, but no document with this id could be found.
|
||||||
|
Available document ids=
|
||||||
|
- ${Object.keys(docsById).sort().join('\n- ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {title, permalink, sidebar_label} = docMetadata;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'link',
|
||||||
|
label: sidebar_label || title,
|
||||||
|
href: permalink,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeItem = (item: SidebarItem): PropSidebarItem => {
|
||||||
|
switch (item.type) {
|
||||||
|
case 'category':
|
||||||
|
return {...item, items: item.items.map(normalizeItem)};
|
||||||
|
case 'ref':
|
||||||
|
case 'doc':
|
||||||
|
return convertDocLink(item);
|
||||||
|
case 'link':
|
||||||
|
default:
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Transform the sidebar so that all sidebar item will be in the
|
||||||
|
// form of 'link' or 'category' only.
|
||||||
|
// This is what will be passed as props to the UI component.
|
||||||
|
return mapValues(loadedVersion.sidebars, (items) => items.map(normalizeItem));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toVersionMetadataProp(
|
||||||
|
loadedVersion: LoadedVersion,
|
||||||
|
): PropVersionMetadata {
|
||||||
|
return {
|
||||||
|
version: loadedVersion.versionName,
|
||||||
|
docsSidebars: toSidebarsProp(loadedVersion),
|
||||||
|
permalinkToSidebar: loadedVersion.permalinkToSidebar,
|
||||||
|
};
|
||||||
|
}
|
|
@ -9,19 +9,47 @@ import flatMap from 'lodash.flatmap';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import importFresh from 'import-fresh';
|
import importFresh from 'import-fresh';
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebars,
|
||||||
SidebarRaw,
|
|
||||||
SidebarItem,
|
SidebarItem,
|
||||||
SidebarItemCategoryRaw,
|
|
||||||
SidebarItemRaw,
|
|
||||||
SidebarItemLink,
|
SidebarItemLink,
|
||||||
SidebarItemDoc,
|
SidebarItemDoc,
|
||||||
SidebarCategoryShorthandRaw,
|
Sidebar,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import {mapValues, flatten, difference} from 'lodash';
|
||||||
|
import {getElementsAround} from '@docusaurus/utils';
|
||||||
|
|
||||||
|
type SidebarItemCategoryJSON = {
|
||||||
|
type: 'category';
|
||||||
|
label: string;
|
||||||
|
items: SidebarItemJSON[];
|
||||||
|
collapsed?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SidebarItemJSON =
|
||||||
|
| string
|
||||||
|
| SidebarCategoryShorthandJSON
|
||||||
|
| SidebarItemDoc
|
||||||
|
| SidebarItemLink
|
||||||
|
| SidebarItemCategoryJSON
|
||||||
|
| {
|
||||||
|
type: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SidebarCategoryShorthandJSON = {
|
||||||
|
[sidebarCategory: string]: SidebarItemJSON[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type SidebarJSON = SidebarCategoryShorthandJSON | SidebarItemJSON[];
|
||||||
|
|
||||||
|
// Sidebar given by user that is not normalized yet. e.g: sidebars.json
|
||||||
|
type SidebarsJSON = {
|
||||||
|
[sidebarId: string]: SidebarJSON;
|
||||||
|
};
|
||||||
|
|
||||||
function isCategoryShorthand(
|
function isCategoryShorthand(
|
||||||
item: SidebarItemRaw,
|
item: SidebarItemJSON,
|
||||||
): item is SidebarCategoryShorthandRaw {
|
): item is SidebarCategoryShorthandJSON {
|
||||||
return typeof item !== 'string' && !item.type;
|
return typeof item !== 'string' && !item.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,8 +60,8 @@ const defaultCategoryCollapsedValue = true;
|
||||||
* Convert {category1: [item1,item2]} shorthand syntax to long-form syntax
|
* Convert {category1: [item1,item2]} shorthand syntax to long-form syntax
|
||||||
*/
|
*/
|
||||||
function normalizeCategoryShorthand(
|
function normalizeCategoryShorthand(
|
||||||
sidebar: SidebarCategoryShorthandRaw,
|
sidebar: SidebarCategoryShorthandJSON,
|
||||||
): SidebarItemCategoryRaw[] {
|
): SidebarItemCategoryJSON[] {
|
||||||
return Object.entries(sidebar).map(([label, items]) => ({
|
return Object.entries(sidebar).map(([label, items]) => ({
|
||||||
type: 'category',
|
type: 'category',
|
||||||
collapsed: defaultCategoryCollapsedValue,
|
collapsed: defaultCategoryCollapsedValue,
|
||||||
|
@ -65,7 +93,7 @@ function assertItem<K extends string>(
|
||||||
|
|
||||||
function assertIsCategory(
|
function assertIsCategory(
|
||||||
item: unknown,
|
item: unknown,
|
||||||
): asserts item is SidebarItemCategoryRaw {
|
): asserts item is SidebarItemCategoryJSON {
|
||||||
assertItem(item, ['items', 'label', 'collapsed']);
|
assertItem(item, ['items', 'label', 'collapsed']);
|
||||||
if (typeof item.label !== 'string') {
|
if (typeof item.label !== 'string') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -112,7 +140,7 @@ function assertIsLink(item: unknown): asserts item is SidebarItemLink {
|
||||||
* Normalizes recursively item and all its children. Ensures that at the end
|
* Normalizes recursively item and all its children. Ensures that at the end
|
||||||
* each item will be an object with the corresponding type.
|
* each item will be an object with the corresponding type.
|
||||||
*/
|
*/
|
||||||
function normalizeItem(item: SidebarItemRaw): SidebarItem[] {
|
function normalizeItem(item: SidebarItemJSON): SidebarItem[] {
|
||||||
if (typeof item === 'string') {
|
if (typeof item === 'string') {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
@ -155,38 +183,119 @@ function normalizeItem(item: SidebarItemRaw): SidebarItem[] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function normalizeSidebar(sidebar: SidebarJSON) {
|
||||||
* Converts sidebars object to mapping to arrays of sidebar item objects.
|
const normalizedSidebar: SidebarItemJSON[] = Array.isArray(sidebar)
|
||||||
*/
|
? sidebar
|
||||||
function normalizeSidebar(sidebars: SidebarRaw): Sidebar {
|
: normalizeCategoryShorthand(sidebar);
|
||||||
return Object.entries(sidebars).reduce(
|
|
||||||
(acc: Sidebar, [sidebarId, sidebar]) => {
|
|
||||||
const normalizedSidebar: SidebarItemRaw[] = Array.isArray(sidebar)
|
|
||||||
? sidebar
|
|
||||||
: normalizeCategoryShorthand(sidebar);
|
|
||||||
|
|
||||||
acc[sidebarId] = flatMap(normalizedSidebar, normalizeItem);
|
return flatMap(normalizedSidebar, normalizeItem);
|
||||||
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function loadSidebars(sidebarPaths?: string[]): Sidebar {
|
function normalizeSidebars(sidebars: SidebarsJSON): Sidebars {
|
||||||
// We don't want sidebars to be cached because of hot reloading.
|
return mapValues(sidebars, normalizeSidebar);
|
||||||
const allSidebars: SidebarRaw = {};
|
}
|
||||||
|
|
||||||
if (!sidebarPaths || !sidebarPaths.length) {
|
// TODO refactor: make async
|
||||||
return {} as Sidebar;
|
export function loadSidebars(sidebarFilePath: string): Sidebars {
|
||||||
|
if (!sidebarFilePath) {
|
||||||
|
throw new Error(`sidebarFilePath not provided: ${sidebarFilePath}`);
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(sidebarFilePath)) {
|
||||||
|
throw new Error(`No sidebar file exist at path: ${sidebarFilePath}`);
|
||||||
|
}
|
||||||
|
// We don't want sidebars to be cached because of hot reloading.
|
||||||
|
const sidebarJson = importFresh(sidebarFilePath) as SidebarsJSON;
|
||||||
|
return normalizeSidebars(sidebarJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
// traverse the sidebar tree in depth to find all doc items, in correct order
|
||||||
|
export function collectSidebarDocItems(sidebar: Sidebar): SidebarItemDoc[] {
|
||||||
|
function collectRecursive(item: SidebarItem): SidebarItemDoc[] {
|
||||||
|
if (item.type === 'doc') {
|
||||||
|
return [item];
|
||||||
|
}
|
||||||
|
if (item.type === 'category') {
|
||||||
|
return flatten(item.items.map(collectRecursive));
|
||||||
|
}
|
||||||
|
// Refs and links should not be shown in navigation.
|
||||||
|
if (item.type === 'ref' || item.type === 'link') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
throw new Error(`unknown sidebar item type = ${item.type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
sidebarPaths.forEach((sidebarPath) => {
|
return flatten(sidebar.map(collectRecursive));
|
||||||
if (sidebarPath && fs.existsSync(sidebarPath)) {
|
}
|
||||||
const sidebar = importFresh(sidebarPath) as SidebarRaw;
|
|
||||||
Object.assign(allSidebars, sidebar);
|
export function collectSidebarsDocIds(
|
||||||
}
|
sidebars: Sidebars,
|
||||||
});
|
): Record<string, string[]> {
|
||||||
|
return mapValues(sidebars, (sidebar) => {
|
||||||
return normalizeSidebar(allSidebars);
|
return collectSidebarDocItems(sidebar).map((docItem) => docItem.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSidebarsUtils(sidebars: Sidebars) {
|
||||||
|
const sidebarNameToDocIds = collectSidebarsDocIds(sidebars);
|
||||||
|
|
||||||
|
function getFirstDocIdOfFirstSidebar(): string | undefined {
|
||||||
|
return Object.values(sidebarNameToDocIds)[0]?.[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSidebarNameByDocId(docId: string): string | undefined {
|
||||||
|
// TODO lookup speed can be optimized
|
||||||
|
const entry = Object.entries(
|
||||||
|
sidebarNameToDocIds,
|
||||||
|
).find(([_sidebarName, docIds]) => docIds.includes(docId));
|
||||||
|
|
||||||
|
return entry?.[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDocNavigation(
|
||||||
|
docId: string,
|
||||||
|
): {
|
||||||
|
sidebarName: string | undefined;
|
||||||
|
previousId: string | undefined;
|
||||||
|
nextId: string | undefined;
|
||||||
|
} {
|
||||||
|
const sidebarName = getSidebarNameByDocId(docId);
|
||||||
|
if (sidebarName) {
|
||||||
|
const docIds = sidebarNameToDocIds[sidebarName];
|
||||||
|
const currentIndex = docIds.indexOf(docId);
|
||||||
|
const {previous, next} = getElementsAround(docIds, currentIndex);
|
||||||
|
return {
|
||||||
|
sidebarName,
|
||||||
|
previousId: previous,
|
||||||
|
nextId: next,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
sidebarName: undefined,
|
||||||
|
previousId: undefined,
|
||||||
|
nextId: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkSidebarsDocIds(validDocIds: string[]) {
|
||||||
|
const allSidebarDocIds = flatten(Object.values(sidebarNameToDocIds));
|
||||||
|
const invalidSidebarDocIds = difference(allSidebarDocIds, validDocIds);
|
||||||
|
if (invalidSidebarDocIds.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Bad sidebars file.
|
||||||
|
These sidebar document ids do not exist:
|
||||||
|
- ${invalidSidebarDocIds.sort().join('\n- ')}\`,
|
||||||
|
|
||||||
|
Available document ids=
|
||||||
|
- ${validDocIds.sort().join('\n- ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getFirstDocIdOfFirstSidebar,
|
||||||
|
getSidebarNameByDocId,
|
||||||
|
getDocNavigation,
|
||||||
|
checkSidebarsDocIds,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ try {
|
||||||
versions = [];
|
versions = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO deprecate in favor of useDocs.ts instead
|
||||||
function useVersioning(): {
|
function useVersioning(): {
|
||||||
versioningEnabled: boolean;
|
versioningEnabled: boolean;
|
||||||
versions: string[];
|
versions: string[];
|
||||||
|
|
|
@ -8,116 +8,91 @@
|
||||||
// eslint-disable-next-line spaced-comment
|
// eslint-disable-next-line spaced-comment
|
||||||
/// <reference types="@docusaurus/module-type-aliases" />
|
/// <reference types="@docusaurus/module-type-aliases" />
|
||||||
|
|
||||||
export type DocsVersion = string | null; // null = unversioned sites
|
export type DocFile = {
|
||||||
|
source: string;
|
||||||
|
content: string;
|
||||||
|
lastUpdate: LastUpdateData;
|
||||||
|
};
|
||||||
|
|
||||||
export interface MetadataOptions {
|
export type VersionName = string;
|
||||||
|
|
||||||
|
export type VersionMetadata = {
|
||||||
|
versionName: VersionName; // 1.0.0
|
||||||
|
versionLabel: string; // Version 1.0.0
|
||||||
|
versionPath: string; // /baseUrl/docs/1.0.0
|
||||||
|
isLast: boolean;
|
||||||
|
docsDirPath: string; // versioned_docs/1.0.0
|
||||||
|
sidebarFilePath: string; // versioned_sidebars/1.0.0.json
|
||||||
|
routePriority: number | undefined; // -1 for the latest docs
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MetadataOptions = {
|
||||||
routeBasePath: string;
|
routeBasePath: string;
|
||||||
homePageId?: string;
|
homePageId?: string;
|
||||||
editUrl?: string;
|
editUrl?: string;
|
||||||
showLastUpdateTime?: boolean;
|
showLastUpdateTime?: boolean;
|
||||||
showLastUpdateAuthor?: boolean;
|
showLastUpdateAuthor?: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface PathOptions {
|
export type PathOptions = {
|
||||||
path: string;
|
path: string;
|
||||||
sidebarPath: string;
|
sidebarPath: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface PluginOptions extends MetadataOptions, PathOptions {
|
export type PluginOptions = MetadataOptions &
|
||||||
id?: string;
|
PathOptions & {
|
||||||
include: string[];
|
id: string;
|
||||||
docLayoutComponent: string;
|
include: string[];
|
||||||
docItemComponent: string;
|
docLayoutComponent: string;
|
||||||
remarkPlugins: ([Function, object] | Function)[];
|
docItemComponent: string;
|
||||||
rehypePlugins: string[];
|
remarkPlugins: ([Function, object] | Function)[];
|
||||||
admonitions: any;
|
rehypePlugins: string[];
|
||||||
disableVersioning: boolean;
|
admonitions: any;
|
||||||
excludeNextVersionDocs: boolean;
|
disableVersioning: boolean;
|
||||||
}
|
excludeNextVersionDocs?: boolean;
|
||||||
|
includeCurrentVersion: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type SidebarItemDoc = {
|
export type SidebarItemDoc = {
|
||||||
type: 'doc' | 'ref';
|
type: 'doc' | 'ref';
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface SidebarItemLink {
|
export type SidebarItemLink = {
|
||||||
type: 'link';
|
type: 'link';
|
||||||
href: string;
|
href: string;
|
||||||
label: string;
|
label: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface SidebarItemCategory {
|
export type SidebarItemCategory = {
|
||||||
type: 'category';
|
type: 'category';
|
||||||
label: string;
|
label: string;
|
||||||
items: SidebarItem[];
|
items: SidebarItem[];
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface SidebarItemCategoryRaw {
|
|
||||||
type: 'category';
|
|
||||||
label: string;
|
|
||||||
items: SidebarItemRaw[];
|
|
||||||
collapsed?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SidebarItem =
|
export type SidebarItem =
|
||||||
| SidebarItemDoc
|
| SidebarItemDoc
|
||||||
| SidebarItemLink
|
| SidebarItemLink
|
||||||
| SidebarItemCategory;
|
| SidebarItemCategory;
|
||||||
|
|
||||||
export type SidebarItemRaw =
|
export type Sidebar = SidebarItem[];
|
||||||
| string
|
|
||||||
| SidebarCategoryShorthandRaw
|
|
||||||
| SidebarItemDoc
|
|
||||||
| SidebarItemLink
|
|
||||||
| SidebarItemCategoryRaw
|
|
||||||
| {
|
|
||||||
type: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface SidebarCategoryShorthandRaw {
|
export type Sidebars = Record<string, Sidebar>;
|
||||||
[sidebarCategory: string]: SidebarItemRaw[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sidebar given by user that is not normalized yet. e.g: sidebars.json
|
export type OrderMetadata = {
|
||||||
export interface SidebarRaw {
|
|
||||||
[sidebarId: string]: SidebarCategoryShorthandRaw | SidebarItemRaw[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Sidebar {
|
|
||||||
[sidebarId: string]: SidebarItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DocsSidebarItemCategory {
|
|
||||||
type: 'category';
|
|
||||||
label: string;
|
|
||||||
items: DocsSidebarItem[];
|
|
||||||
collapsed?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DocsSidebarItem = SidebarItemLink | DocsSidebarItemCategory;
|
|
||||||
|
|
||||||
export interface DocsSidebar {
|
|
||||||
[sidebarId: string]: DocsSidebarItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OrderMetadata {
|
|
||||||
previous?: string;
|
previous?: string;
|
||||||
next?: string;
|
next?: string;
|
||||||
sidebar?: string;
|
sidebar?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface Order {
|
export type LastUpdateData = {
|
||||||
[id: string]: OrderMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LastUpdateData {
|
|
||||||
lastUpdatedAt?: number;
|
lastUpdatedAt?: number;
|
||||||
lastUpdatedBy?: string;
|
lastUpdatedBy?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface MetadataRaw extends LastUpdateData {
|
export type DocMetadataBase = LastUpdateData & {
|
||||||
|
version: VersionName;
|
||||||
unversionedId: string;
|
unversionedId: string;
|
||||||
id: string;
|
id: string;
|
||||||
isDocsHomePage: boolean;
|
isDocsHomePage: boolean;
|
||||||
|
@ -128,67 +103,38 @@ export interface MetadataRaw extends LastUpdateData {
|
||||||
permalink: string;
|
permalink: string;
|
||||||
sidebar_label?: string;
|
sidebar_label?: string;
|
||||||
editUrl?: string | null;
|
editUrl?: string | null;
|
||||||
version?: string;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export interface Paginator {
|
export type DocNavLink = {
|
||||||
title: string;
|
title: string;
|
||||||
permalink: string;
|
permalink: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface Metadata extends MetadataRaw {
|
export type DocMetadata = DocMetadataBase & {
|
||||||
sidebar?: string;
|
sidebar?: string;
|
||||||
previous?: Paginator;
|
previous?: DocNavLink;
|
||||||
next?: Paginator;
|
next?: DocNavLink;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface DocsMetadata {
|
export type SourceToPermalink = {
|
||||||
[id: string]: Metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DocsMetadataRaw {
|
|
||||||
[id: string]: MetadataRaw;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SourceToPermalink {
|
|
||||||
[source: string]: string;
|
[source: string]: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface PermalinkToSidebar {
|
export type PermalinkToSidebar = {
|
||||||
[permalink: string]: string;
|
[permalink: string]: string;
|
||||||
}
|
|
||||||
|
|
||||||
export interface VersionToSidebars {
|
|
||||||
[version: string]: Set<string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LoadedContent {
|
|
||||||
docsMetadata: DocsMetadata;
|
|
||||||
docsDir: string;
|
|
||||||
docsSidebars: DocsSidebar;
|
|
||||||
permalinkToSidebar: PermalinkToSidebar;
|
|
||||||
versionToSidebars: VersionToSidebars;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DocsBaseMetadata = Pick<
|
|
||||||
LoadedContent,
|
|
||||||
'docsSidebars' | 'permalinkToSidebar'
|
|
||||||
> & {
|
|
||||||
version: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VersioningEnv = {
|
export type LoadedVersion = VersionMetadata & {
|
||||||
enabled: boolean;
|
versionPath: string;
|
||||||
latestVersion: string | null;
|
mainDocId: string;
|
||||||
versions: string[];
|
docs: DocMetadata[];
|
||||||
docsDir: string;
|
sidebars: Sidebars;
|
||||||
sidebarsDir: string;
|
permalinkToSidebar: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Env {
|
export type LoadedContent = {
|
||||||
versioning: VersioningEnv;
|
loadedVersions: LoadedVersion[];
|
||||||
// TODO: translation
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export type GlobalDoc = {
|
export type GlobalDoc = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -196,7 +142,9 @@ export type GlobalDoc = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GlobalVersion = {
|
export type GlobalVersion = {
|
||||||
name: DocsVersion;
|
name: VersionName;
|
||||||
|
label: string;
|
||||||
|
isLast: boolean;
|
||||||
path: string;
|
path: string;
|
||||||
mainDocId: string; // home doc (if docs homepage configured), or first doc
|
mainDocId: string; // home doc (if docs homepage configured), or first doc
|
||||||
docs: GlobalDoc[];
|
docs: GlobalDoc[];
|
||||||
|
@ -204,6 +152,39 @@ export type GlobalVersion = {
|
||||||
|
|
||||||
export type GlobalPluginData = {
|
export type GlobalPluginData = {
|
||||||
path: string;
|
path: string;
|
||||||
latestVersionName: DocsVersion;
|
|
||||||
versions: GlobalVersion[];
|
versions: GlobalVersion[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PropVersionMetadata = {
|
||||||
|
version: VersionName;
|
||||||
|
docsSidebars: PropSidebars;
|
||||||
|
permalinkToSidebar: PermalinkToSidebar;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PropSidebarItemLink = SidebarItemLink; // same
|
||||||
|
|
||||||
|
export type PropSidebarItemCategory = {
|
||||||
|
type: 'category';
|
||||||
|
label: string;
|
||||||
|
items: PropSidebarItem[];
|
||||||
|
collapsed?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PropSidebarItem = PropSidebarItemLink | PropSidebarItemCategory;
|
||||||
|
|
||||||
|
export type PropSidebars = {
|
||||||
|
[sidebarId: string]: PropSidebarItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BrokenMarkdownLink = {
|
||||||
|
filePath: string;
|
||||||
|
version: VersionMetadata;
|
||||||
|
link: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DocsMarkdownOption = {
|
||||||
|
versionsMetadata: VersionMetadata[];
|
||||||
|
siteDir: string;
|
||||||
|
sourceToPermalink: SourceToPermalink;
|
||||||
|
onBrokenMarkdownLink: (brokenMarkdownLink: BrokenMarkdownLink) => void;
|
||||||
|
};
|
||||||
|
|
259
packages/docusaurus-plugin-content-docs/src/versions.ts
Normal file
259
packages/docusaurus-plugin-content-docs/src/versions.ts
Normal file
|
@ -0,0 +1,259 @@
|
||||||
|
/**
|
||||||
|
* 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 path from 'path';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import {PluginOptions, VersionMetadata} from './types';
|
||||||
|
import {
|
||||||
|
VERSIONS_JSON_FILE,
|
||||||
|
VERSIONED_DOCS_DIR,
|
||||||
|
VERSIONED_SIDEBARS_DIR,
|
||||||
|
CURRENT_VERSION_NAME,
|
||||||
|
} from './constants';
|
||||||
|
|
||||||
|
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
|
||||||
|
import {LoadContext} from '@docusaurus/types';
|
||||||
|
import {normalizeUrl} from '@docusaurus/utils';
|
||||||
|
|
||||||
|
// retro-compatibility: no prefix for the default plugin id
|
||||||
|
function addPluginIdPrefix(fileOrDir: string, pluginId: string): string {
|
||||||
|
if (pluginId === DEFAULT_PLUGIN_ID) {
|
||||||
|
return fileOrDir;
|
||||||
|
} else {
|
||||||
|
return `${pluginId}_${fileOrDir}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVersionedDocsDirPath(
|
||||||
|
siteDir: string,
|
||||||
|
pluginId: string,
|
||||||
|
): string {
|
||||||
|
return path.join(siteDir, addPluginIdPrefix(VERSIONED_DOCS_DIR, pluginId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVersionedSidebarsDirPath(
|
||||||
|
siteDir: string,
|
||||||
|
pluginId: string,
|
||||||
|
): string {
|
||||||
|
return path.join(
|
||||||
|
siteDir,
|
||||||
|
addPluginIdPrefix(VERSIONED_SIDEBARS_DIR, pluginId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVersionsFilePath(siteDir: string, pluginId: string): string {
|
||||||
|
return path.join(siteDir, addPluginIdPrefix(VERSIONS_JSON_FILE, pluginId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValidVersionString(version: unknown): asserts version is string {
|
||||||
|
if (typeof version !== 'string') {
|
||||||
|
throw new Error(
|
||||||
|
`versions should be strings. Found type=[${typeof version}] for version=[${version}]`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Should we forbid versions with special chars like / ?
|
||||||
|
if (version.trim().length === 0) {
|
||||||
|
throw new Error(`Invalid version=[${version}]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValidVersionArray(
|
||||||
|
versionArray: unknown,
|
||||||
|
): asserts versionArray is string[] {
|
||||||
|
if (!(versionArray instanceof Array)) {
|
||||||
|
throw new Error(
|
||||||
|
`The versions file should contain an array of versions! Found content=${JSON.stringify(
|
||||||
|
versionArray,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
versionArray.forEach(ensureValidVersionString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO not easy to make async due to many deps
|
||||||
|
function readVersionsFile(siteDir: string, pluginId: string): string[] | null {
|
||||||
|
const versionsFilePath = getVersionsFilePath(siteDir, pluginId);
|
||||||
|
if (fs.existsSync(versionsFilePath)) {
|
||||||
|
const content = JSON.parse(fs.readFileSync(versionsFilePath, 'utf8'));
|
||||||
|
ensureValidVersionArray(content);
|
||||||
|
return content;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO not easy to make async due to many deps
|
||||||
|
function readVersionNames(
|
||||||
|
siteDir: string,
|
||||||
|
options: Pick<
|
||||||
|
PluginOptions,
|
||||||
|
'id' | 'disableVersioning' | 'includeCurrentVersion'
|
||||||
|
>,
|
||||||
|
): string[] {
|
||||||
|
const versionFileContent = readVersionsFile(siteDir, options.id);
|
||||||
|
|
||||||
|
if (!versionFileContent && options.disableVersioning) {
|
||||||
|
throw new Error(
|
||||||
|
`Docs: using disableVersioning=${options.disableVersioning} option on a non-versioned site does not make sense`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const versions = options.disableVersioning ? [] : versionFileContent ?? [];
|
||||||
|
|
||||||
|
// We add the current version at the beginning, unless
|
||||||
|
// - user don't want to
|
||||||
|
// - it's been explicitly added to versions.json
|
||||||
|
if (
|
||||||
|
options.includeCurrentVersion &&
|
||||||
|
!versions.includes(CURRENT_VERSION_NAME)
|
||||||
|
) {
|
||||||
|
versions.unshift(CURRENT_VERSION_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (versions.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
`It is not possible to use docs without any version. Please check the configuration of these options: includeCurrentVersion=${options.includeCurrentVersion} disableVersioning=${options.disableVersioning}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return versions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVersionMetadataPaths({
|
||||||
|
versionName,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
versionName: string;
|
||||||
|
context: Pick<LoadContext, 'siteDir'>;
|
||||||
|
options: Pick<PluginOptions, 'id' | 'path' | 'sidebarPath'>;
|
||||||
|
}): Pick<VersionMetadata, 'docsDirPath' | 'sidebarFilePath'> {
|
||||||
|
const isCurrentVersion = versionName === CURRENT_VERSION_NAME;
|
||||||
|
|
||||||
|
const docsDirPath = isCurrentVersion
|
||||||
|
? path.resolve(context.siteDir, options.path)
|
||||||
|
: path.join(
|
||||||
|
getVersionedDocsDirPath(context.siteDir, options.id),
|
||||||
|
`version-${versionName}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const sidebarFilePath = isCurrentVersion
|
||||||
|
? path.resolve(context.siteDir, options.sidebarPath)
|
||||||
|
: path.join(
|
||||||
|
getVersionedSidebarsDirPath(context.siteDir, options.id),
|
||||||
|
`version-${versionName}-sidebars.json`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {docsDirPath, sidebarFilePath};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createVersionMetadata({
|
||||||
|
versionName,
|
||||||
|
isLast,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
versionName: string;
|
||||||
|
isLast: boolean;
|
||||||
|
context: Pick<LoadContext, 'siteDir' | 'baseUrl'>;
|
||||||
|
options: Pick<PluginOptions, 'id' | 'path' | 'sidebarPath' | 'routeBasePath'>;
|
||||||
|
}): VersionMetadata {
|
||||||
|
const {sidebarFilePath, docsDirPath} = getVersionMetadataPaths({
|
||||||
|
versionName,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO hardcoded for retro-compatibility
|
||||||
|
// TODO Need to make this configurable
|
||||||
|
const versionLabel =
|
||||||
|
versionName === CURRENT_VERSION_NAME ? 'Next' : versionName;
|
||||||
|
const versionPathPart = isLast
|
||||||
|
? ''
|
||||||
|
: versionName === CURRENT_VERSION_NAME
|
||||||
|
? 'next'
|
||||||
|
: versionName;
|
||||||
|
|
||||||
|
const versionPath = normalizeUrl([
|
||||||
|
context.baseUrl,
|
||||||
|
options.routeBasePath,
|
||||||
|
versionPathPart,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Because /docs/:route` should always be after `/docs/versionName/:route`.
|
||||||
|
const routePriority = versionPathPart === '' ? -1 : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
versionName,
|
||||||
|
versionLabel,
|
||||||
|
versionPath,
|
||||||
|
isLast,
|
||||||
|
routePriority,
|
||||||
|
sidebarFilePath,
|
||||||
|
docsDirPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkVersionMetadataPaths({
|
||||||
|
versionName,
|
||||||
|
docsDirPath,
|
||||||
|
sidebarFilePath,
|
||||||
|
}: VersionMetadata) {
|
||||||
|
if (!fs.existsSync(docsDirPath)) {
|
||||||
|
throw new Error(
|
||||||
|
`The docs folder does not exist for version [${versionName}]. A docs folder is expected to be found at ${docsDirPath}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(sidebarFilePath)) {
|
||||||
|
throw new Error(
|
||||||
|
`The sidebar file does not exist for version [${versionName}]. A sidebar file is expected to be found at ${sidebarFilePath}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO for retrocompatibility with existing behavior
|
||||||
|
// We should make this configurable
|
||||||
|
// "last version" is not a very good concept nor api surface
|
||||||
|
function getLastVersionName(versionNames: string[]) {
|
||||||
|
if (versionNames.length === 1) {
|
||||||
|
return versionNames[0];
|
||||||
|
} else {
|
||||||
|
return versionNames.filter(
|
||||||
|
(versionName) => versionName !== CURRENT_VERSION_NAME,
|
||||||
|
)[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readVersionsMetadata({
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
context: Pick<LoadContext, 'siteDir' | 'baseUrl'>;
|
||||||
|
options: Pick<
|
||||||
|
PluginOptions,
|
||||||
|
| 'id'
|
||||||
|
| 'path'
|
||||||
|
| 'sidebarPath'
|
||||||
|
| 'routeBasePath'
|
||||||
|
| 'includeCurrentVersion'
|
||||||
|
| 'disableVersioning'
|
||||||
|
>;
|
||||||
|
}): VersionMetadata[] {
|
||||||
|
const versionNames = readVersionNames(context.siteDir, options);
|
||||||
|
const lastVersionName = getLastVersionName(versionNames);
|
||||||
|
const versionsMetadata = versionNames.map((versionName) =>
|
||||||
|
createVersionMetadata({
|
||||||
|
versionName,
|
||||||
|
isLast: versionName === lastVersionName,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
versionsMetadata.forEach(checkVersionMetadataPaths);
|
||||||
|
return versionsMetadata;
|
||||||
|
}
|
|
@ -20,11 +20,11 @@ import styles from './styles.module.css';
|
||||||
|
|
||||||
function DocPageContent({
|
function DocPageContent({
|
||||||
currentDocRoute,
|
currentDocRoute,
|
||||||
docsMetadata,
|
versionMetadata,
|
||||||
children,
|
children,
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const {siteConfig, isClient} = useDocusaurusContext();
|
const {siteConfig, isClient} = useDocusaurusContext();
|
||||||
const {permalinkToSidebar, docsSidebars, version} = docsMetadata;
|
const {permalinkToSidebar, docsSidebars, version} = versionMetadata;
|
||||||
const sidebarName = permalinkToSidebar[currentDocRoute.path];
|
const sidebarName = permalinkToSidebar[currentDocRoute.path];
|
||||||
const sidebar = docsSidebars[sidebarName];
|
const sidebar = docsSidebars[sidebarName];
|
||||||
return (
|
return (
|
||||||
|
@ -52,7 +52,7 @@ function DocPageContent({
|
||||||
function DocPage(props) {
|
function DocPage(props) {
|
||||||
const {
|
const {
|
||||||
route: {routes: docRoutes},
|
route: {routes: docRoutes},
|
||||||
docsMetadata,
|
versionMetadata,
|
||||||
location,
|
location,
|
||||||
} = props;
|
} = props;
|
||||||
const currentDocRoute = docRoutes.find((docRoute) =>
|
const currentDocRoute = docRoutes.find((docRoute) =>
|
||||||
|
@ -64,7 +64,7 @@ function DocPage(props) {
|
||||||
return (
|
return (
|
||||||
<DocPageContent
|
<DocPageContent
|
||||||
currentDocRoute={currentDocRoute}
|
currentDocRoute={currentDocRoute}
|
||||||
docsMetadata={docsMetadata}>
|
versionMetadata={versionMetadata}>
|
||||||
{renderRoutes(docRoutes)}
|
{renderRoutes(docRoutes)}
|
||||||
</DocPageContent>
|
</DocPageContent>
|
||||||
);
|
);
|
||||||
|
|
|
@ -52,18 +52,21 @@ function DocVersionSuggestions(): JSX.Element {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="alert alert--warning margin-bottom--md" role="alert">
|
<div className="alert alert--warning margin-bottom--md" role="alert">
|
||||||
{activeVersionName === 'next' ? (
|
{
|
||||||
<div>
|
// TODO need refactoring
|
||||||
This is unreleased documentation for {siteTitle}{' '}
|
activeVersionName === 'current' ? (
|
||||||
<strong>{activeVersionName}</strong> version.
|
<div>
|
||||||
</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
|
<div>
|
||||||
maintained.
|
This is documentation for {siteTitle}{' '}
|
||||||
</div>
|
<strong>v{activeVersionName}</strong>, which is no longer actively
|
||||||
)}
|
maintained.
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
<div className="margin-top--md">
|
<div className="margin-top--md">
|
||||||
For up-to-date documentation, see the{' '}
|
For up-to-date documentation, see the{' '}
|
||||||
<strong>
|
<strong>
|
||||||
|
|
|
@ -13,16 +13,13 @@ import {
|
||||||
useActiveDocContext,
|
useActiveDocContext,
|
||||||
} from '@theme/hooks/useDocs';
|
} from '@theme/hooks/useDocs';
|
||||||
|
|
||||||
const versionLabel = (version, nextVersionLabel) =>
|
|
||||||
version.name === 'next' ? nextVersionLabel : version.name;
|
|
||||||
|
|
||||||
const getVersionMainDoc = (version) =>
|
const getVersionMainDoc = (version) =>
|
||||||
version.docs.find((doc) => doc.id === version.mainDocId);
|
version.docs.find((doc) => doc.id === version.mainDocId);
|
||||||
|
|
||||||
export default function DocsVersionDropdownNavbarItem({
|
export default function DocsVersionDropdownNavbarItem({
|
||||||
mobile,
|
mobile,
|
||||||
docsPluginId,
|
docsPluginId,
|
||||||
nextVersionLabel,
|
nextVersionLabel: _unused, // TODO legacy, remove asap
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
const activeDocContext = useActiveDocContext(docsPluginId);
|
const activeDocContext = useActiveDocContext(docsPluginId);
|
||||||
|
@ -37,7 +34,7 @@ export default function DocsVersionDropdownNavbarItem({
|
||||||
getVersionMainDoc(version);
|
getVersionMainDoc(version);
|
||||||
return {
|
return {
|
||||||
isNavLink: true,
|
isNavLink: true,
|
||||||
label: versionLabel(version, nextVersionLabel),
|
label: version.label,
|
||||||
to: versionDoc.path,
|
to: versionDoc.path,
|
||||||
isActive: () => version === activeDocContext?.activeVersion,
|
isActive: () => version === activeDocContext?.activeVersion,
|
||||||
};
|
};
|
||||||
|
@ -46,9 +43,7 @@ export default function DocsVersionDropdownNavbarItem({
|
||||||
const dropdownVersion = activeDocContext.activeVersion ?? latestVersion;
|
const dropdownVersion = activeDocContext.activeVersion ?? latestVersion;
|
||||||
|
|
||||||
// Mobile is handled a bit differently
|
// Mobile is handled a bit differently
|
||||||
const dropdownLabel = mobile
|
const dropdownLabel = mobile ? 'Versions' : dropdownVersion.label;
|
||||||
? 'Versions'
|
|
||||||
: versionLabel(dropdownVersion, nextVersionLabel);
|
|
||||||
const dropdownTo = mobile
|
const dropdownTo = mobile
|
||||||
? undefined
|
? undefined
|
||||||
: getVersionMainDoc(dropdownVersion).path;
|
: getVersionMainDoc(dropdownVersion).path;
|
||||||
|
|
|
@ -12,20 +12,17 @@ import {useActiveVersion, useLatestVersion} from '@theme/hooks/useDocs';
|
||||||
const getVersionMainDoc = (version) =>
|
const getVersionMainDoc = (version) =>
|
||||||
version.docs.find((doc) => doc.id === version.mainDocId);
|
version.docs.find((doc) => doc.id === version.mainDocId);
|
||||||
|
|
||||||
const versionLabel = (version, nextVersionLabel) =>
|
|
||||||
version.name === 'next' ? nextVersionLabel : version.name;
|
|
||||||
|
|
||||||
export default function DocsVersionNavbarItem({
|
export default function DocsVersionNavbarItem({
|
||||||
label: staticLabel,
|
label: staticLabel,
|
||||||
to: staticTo,
|
to: staticTo,
|
||||||
docsPluginId,
|
docsPluginId,
|
||||||
nextVersionLabel,
|
nextVersionLabel: _unused, // TODO legacy, remove asap
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
const activeVersion = useActiveVersion(docsPluginId);
|
const activeVersion = useActiveVersion(docsPluginId);
|
||||||
const latestVersion = useLatestVersion(docsPluginId);
|
const latestVersion = useLatestVersion(docsPluginId);
|
||||||
const version = activeVersion ?? latestVersion;
|
const version = activeVersion ?? latestVersion;
|
||||||
const label = staticLabel ?? versionLabel(version, nextVersionLabel);
|
const label = staticLabel ?? version.label;
|
||||||
const path = staticTo ?? getVersionMainDoc(version).path;
|
const path = staticTo ?? getVersionMainDoc(version).path;
|
||||||
return <DefaultNavbarItem {...props} label={label} to={path} />;
|
return <DefaultNavbarItem {...props} label={label} to={path} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,14 +50,13 @@ const DocsVersionNavbarItemSchema = Joi.object({
|
||||||
label: Joi.string(),
|
label: Joi.string(),
|
||||||
to: Joi.string(),
|
to: Joi.string(),
|
||||||
docsPluginId: Joi.string(),
|
docsPluginId: Joi.string(),
|
||||||
nextVersionLabel: Joi.string().default('Next'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const DocsVersionDropdownNavbarItemSchema = Joi.object({
|
const DocsVersionDropdownNavbarItemSchema = Joi.object({
|
||||||
type: Joi.string().equal('docsVersionDropdown').required(),
|
type: Joi.string().equal('docsVersionDropdown').required(),
|
||||||
position: NavbarItemPosition,
|
position: NavbarItemPosition,
|
||||||
docsPluginId: Joi.string(),
|
docsPluginId: Joi.string(),
|
||||||
nextVersionLabel: Joi.string().default('Next'),
|
nextVersionLabel: Joi.string().default('Next'), // TODO remove soon
|
||||||
});
|
});
|
||||||
|
|
||||||
// Can this be made easier? :/
|
// Can this be made easier? :/
|
||||||
|
|
|
@ -26,6 +26,7 @@ import {
|
||||||
removePrefix,
|
removePrefix,
|
||||||
getFilePathForRoutePath,
|
getFilePathForRoutePath,
|
||||||
addLeadingSlash,
|
addLeadingSlash,
|
||||||
|
getElementsAround,
|
||||||
} from '../index';
|
} from '../index';
|
||||||
|
|
||||||
describe('load utils', () => {
|
describe('load utils', () => {
|
||||||
|
@ -477,3 +478,37 @@ describe('getFilePathForRoutePath', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getElementsAround', () => {
|
||||||
|
test('can return elements around', () => {
|
||||||
|
expect(getElementsAround(['a', 'b', 'c', 'd'], 0)).toEqual({
|
||||||
|
previous: undefined,
|
||||||
|
next: 'b',
|
||||||
|
});
|
||||||
|
expect(getElementsAround(['a', 'b', 'c', 'd'], 1)).toEqual({
|
||||||
|
previous: 'a',
|
||||||
|
next: 'c',
|
||||||
|
});
|
||||||
|
expect(getElementsAround(['a', 'b', 'c', 'd'], 2)).toEqual({
|
||||||
|
previous: 'b',
|
||||||
|
next: 'd',
|
||||||
|
});
|
||||||
|
expect(getElementsAround(['a', 'b', 'c', 'd'], 3)).toEqual({
|
||||||
|
previous: 'c',
|
||||||
|
next: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws if bad index is provided', () => {
|
||||||
|
expect(() =>
|
||||||
|
getElementsAround(['a', 'b', 'c', 'd'], -1),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Valid aroundIndex for array (of size 4) are between 0 and 3, but you provided aroundIndex=-1"`,
|
||||||
|
);
|
||||||
|
expect(() =>
|
||||||
|
getElementsAround(['a', 'b', 'c', 'd'], 4),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Valid aroundIndex for array (of size 4) are between 0 and 3, but you provided aroundIndex=4"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -403,3 +403,22 @@ export function getFilePathForRoutePath(routePath: string): string {
|
||||||
const filePath = path.dirname(routePath);
|
const filePath = path.dirname(routePath);
|
||||||
return path.join(filePath, `${fileName}/index.html`);
|
return path.join(filePath, `${fileName}/index.html`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getElementsAround<T extends unknown>(
|
||||||
|
array: T[],
|
||||||
|
aroundIndex: number,
|
||||||
|
): {
|
||||||
|
next: T | undefined;
|
||||||
|
previous: T | undefined;
|
||||||
|
} {
|
||||||
|
const min = 0;
|
||||||
|
const max = array.length - 1;
|
||||||
|
if (aroundIndex < min || aroundIndex > max) {
|
||||||
|
throw new Error(
|
||||||
|
`Valid aroundIndex for array (of size ${array.length}) are between ${min} and ${max}, but you provided aroundIndex=${aroundIndex}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const previous = aroundIndex === min ? undefined : array[aroundIndex - 1];
|
||||||
|
const next = aroundIndex === max ? undefined : array[aroundIndex + 1];
|
||||||
|
return {previous, next};
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -189,7 +189,7 @@ High-level overview about themes:
|
||||||
Related pieces
|
Related pieces
|
||||||
---
|
---
|
||||||
|
|
||||||
- [Advanced Guides – Themes](advanced-themes.md)
|
- [Advanced Guides – Themes](using-themes.md)
|
||||||
- [Lifecycle APIs](lifecycle-apis.md)
|
- [Lifecycle APIs](lifecycle-apis.md)
|
||||||
|
|
||||||
References
|
References
|
||||||
|
|
|
@ -182,7 +182,7 @@ High-level overview about themes:
|
||||||
Related pieces
|
Related pieces
|
||||||
---
|
---
|
||||||
|
|
||||||
- [Advanced Guides – Themes](advanced-themes.md)
|
- [Advanced Guides – Themes](using-themes.md)
|
||||||
- [Lifecycle APIs](lifecycle-apis.md)
|
- [Lifecycle APIs](lifecycle-apis.md)
|
||||||
|
|
||||||
References
|
References
|
||||||
|
|
|
@ -184,7 +184,7 @@ High-level overview about themes:
|
||||||
Related pieces
|
Related pieces
|
||||||
---
|
---
|
||||||
|
|
||||||
- [Advanced Guides – Themes](advanced-themes.md)
|
- [Advanced Guides – Themes](using-themes.md)
|
||||||
- [Lifecycle APIs](lifecycle-apis.md)
|
- [Lifecycle APIs](lifecycle-apis.md)
|
||||||
|
|
||||||
References
|
References
|
||||||
|
|
|
@ -184,7 +184,7 @@ High-level overview about themes:
|
||||||
Related pieces
|
Related pieces
|
||||||
---
|
---
|
||||||
|
|
||||||
- [Advanced Guides – Themes](advanced-themes.md)
|
- [Advanced Guides – Themes](using-themes.md)
|
||||||
- [Lifecycle APIs](lifecycle-apis.md)
|
- [Lifecycle APIs](lifecycle-apis.md)
|
||||||
|
|
||||||
References
|
References
|
||||||
|
|
|
@ -184,7 +184,7 @@ High-level overview about themes:
|
||||||
Related pieces
|
Related pieces
|
||||||
---
|
---
|
||||||
|
|
||||||
- [Advanced Guides – Themes](advanced-themes.md)
|
- [Advanced Guides – Themes](using-themes.md)
|
||||||
- [Lifecycle APIs](lifecycle-apis.md)
|
- [Lifecycle APIs](lifecycle-apis.md)
|
||||||
|
|
||||||
References
|
References
|
||||||
|
|
|
@ -189,7 +189,7 @@ High-level overview about themes:
|
||||||
Related pieces
|
Related pieces
|
||||||
---
|
---
|
||||||
|
|
||||||
- [Advanced Guides – Themes](advanced-themes.md)
|
- [Advanced Guides – Themes](using-themes.md)
|
||||||
- [Lifecycle APIs](lifecycle-apis.md)
|
- [Lifecycle APIs](lifecycle-apis.md)
|
||||||
|
|
||||||
References
|
References
|
||||||
|
|
|
@ -189,7 +189,7 @@ High-level overview about themes:
|
||||||
Related pieces
|
Related pieces
|
||||||
---
|
---
|
||||||
|
|
||||||
- [Advanced Guides – Themes](advanced-themes.md)
|
- [Advanced Guides – Themes](using-themes.md)
|
||||||
- [Lifecycle APIs](lifecycle-apis.md)
|
- [Lifecycle APIs](lifecycle-apis.md)
|
||||||
|
|
||||||
References
|
References
|
||||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -4074,6 +4074,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109"
|
resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109"
|
||||||
integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==
|
integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==
|
||||||
|
|
||||||
|
"@types/picomatch@^2.2.1":
|
||||||
|
version "2.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/picomatch/-/picomatch-2.2.1.tgz#f9e5a5e6ad03996832975ab7eadfa35791ca2a8f"
|
||||||
|
integrity sha512-26/tQcDmJXYHiaWAAIjnTVL5nwrT+IVaqFZIbBImAuKk/r/j1r/1hmZ7uaOzG6IknqP3QHcNNQ6QO8Vp28lUoA==
|
||||||
|
|
||||||
"@types/prettier@^1.19.0":
|
"@types/prettier@^1.19.0":
|
||||||
version "1.19.1"
|
version "1.19.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.19.1.tgz#33509849f8e679e4add158959fdb086440e9553f"
|
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.19.1.tgz#33509849f8e679e4add158959fdb086440e9553f"
|
||||||
|
@ -21279,6 +21284,11 @@ utila@^0.4.0, utila@~0.4:
|
||||||
resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
|
resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
|
||||||
integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=
|
integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=
|
||||||
|
|
||||||
|
utility-types@^3.10.0:
|
||||||
|
version "3.10.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b"
|
||||||
|
integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==
|
||||||
|
|
||||||
utils-merge@1.0.1:
|
utils-merge@1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||||
|
|
Loading…
Add table
Reference in a new issue