mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-29 10:17:55 +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},
|
||||
],
|
||||
'no-unused-vars': OFF,
|
||||
'no-nested-ternary': WARNING,
|
||||
'@typescript-eslint/no-unused-vars': [ERROR, {argsIgnorePattern: '^_'}],
|
||||
'@typescript-eslint/ban-ts-comment': [
|
||||
ERROR,
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^2.0.0-alpha.61",
|
||||
"@types/hapi__joi": "^17.1.2",
|
||||
"@types/picomatch": "^2.2.1",
|
||||
"commander": "^5.0.0",
|
||||
"picomatch": "^2.1.1"
|
||||
},
|
||||
|
@ -37,7 +38,9 @@
|
|||
"lodash.pickby": "^4.6.0",
|
||||
"lodash.sortby": "^4.6.0",
|
||||
"remark-admonitions": "^1.2.1",
|
||||
"shelljs": "^0.8.4"
|
||||
"shelljs": "^0.8.4",
|
||||
"utility-types": "^3.10.0",
|
||||
"webpack": "^4.41.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"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 {docsVersion} from '../version';
|
||||
import {cliDocsVersionCommand} from '../cli';
|
||||
import {PathOptions} from '../types';
|
||||
import fs from 'fs-extra';
|
||||
import {
|
||||
getVersionedDocsDir,
|
||||
getVersionsJSONFile,
|
||||
getVersionedSidebarsDir,
|
||||
} from '../env';
|
||||
getVersionedDocsDirPath,
|
||||
getVersionsFilePath,
|
||||
getVersionedSidebarsDirPath,
|
||||
} from '../versions';
|
||||
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
|
||||
|
||||
const fixtureDir = path.join(__dirname, '__fixtures__');
|
||||
|
@ -28,17 +28,32 @@ describe('docsVersion', () => {
|
|||
|
||||
test('no version tag provided', () => {
|
||||
expect(() =>
|
||||
docsVersion(null, simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
|
||||
cliDocsVersionCommand(
|
||||
null,
|
||||
simpleSiteDir,
|
||||
DEFAULT_PLUGIN_ID,
|
||||
DEFAULT_OPTIONS,
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[docs] No version tag specified!. Pass the version you wish to create as an argument. Ex: 1.0.0"`,
|
||||
);
|
||||
expect(() =>
|
||||
docsVersion(undefined, simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
|
||||
cliDocsVersionCommand(
|
||||
undefined,
|
||||
simpleSiteDir,
|
||||
DEFAULT_PLUGIN_ID,
|
||||
DEFAULT_OPTIONS,
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[docs] No version tag specified!. Pass the version you wish to create as an argument. Ex: 1.0.0"`,
|
||||
);
|
||||
expect(() =>
|
||||
docsVersion('', simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
|
||||
cliDocsVersionCommand(
|
||||
'',
|
||||
simpleSiteDir,
|
||||
DEFAULT_PLUGIN_ID,
|
||||
DEFAULT_OPTIONS,
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[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', () => {
|
||||
expect(() =>
|
||||
docsVersion('foo/bar', simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
|
||||
cliDocsVersionCommand(
|
||||
'foo/bar',
|
||||
simpleSiteDir,
|
||||
DEFAULT_PLUGIN_ID,
|
||||
DEFAULT_OPTIONS,
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[docs] Invalid version tag specified! Do not include slash (/) or (\\\\). Try something like: 1.0.0"`,
|
||||
);
|
||||
expect(() =>
|
||||
docsVersion(
|
||||
cliDocsVersionCommand(
|
||||
'foo\\bar',
|
||||
simpleSiteDir,
|
||||
DEFAULT_PLUGIN_ID,
|
||||
|
@ -64,7 +84,7 @@ describe('docsVersion', () => {
|
|||
|
||||
test('version tag should not be too long', () => {
|
||||
expect(() =>
|
||||
docsVersion(
|
||||
cliDocsVersionCommand(
|
||||
'a'.repeat(255),
|
||||
simpleSiteDir,
|
||||
DEFAULT_PLUGIN_ID,
|
||||
|
@ -77,12 +97,22 @@ describe('docsVersion', () => {
|
|||
|
||||
test('version tag should not be a dot or two dots', () => {
|
||||
expect(() =>
|
||||
docsVersion('..', simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
|
||||
cliDocsVersionCommand(
|
||||
'..',
|
||||
simpleSiteDir,
|
||||
DEFAULT_PLUGIN_ID,
|
||||
DEFAULT_OPTIONS,
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[docs] Invalid version tag specified! Do not name your version \\".\\" or \\"..\\". Try something like: 1.0.0"`,
|
||||
);
|
||||
expect(() =>
|
||||
docsVersion('.', simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
|
||||
cliDocsVersionCommand(
|
||||
'.',
|
||||
simpleSiteDir,
|
||||
DEFAULT_PLUGIN_ID,
|
||||
DEFAULT_OPTIONS,
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[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', () => {
|
||||
expect(() =>
|
||||
docsVersion(
|
||||
cliDocsVersionCommand(
|
||||
'<foo|bar>',
|
||||
simpleSiteDir,
|
||||
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"`,
|
||||
);
|
||||
expect(() =>
|
||||
docsVersion(
|
||||
cliDocsVersionCommand(
|
||||
'foo\x00bar',
|
||||
simpleSiteDir,
|
||||
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"`,
|
||||
);
|
||||
expect(() =>
|
||||
docsVersion('foo:bar', simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
|
||||
cliDocsVersionCommand(
|
||||
'foo:bar',
|
||||
simpleSiteDir,
|
||||
DEFAULT_PLUGIN_ID,
|
||||
DEFAULT_OPTIONS,
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[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', () => {
|
||||
expect(() =>
|
||||
docsVersion(
|
||||
cliDocsVersionCommand(
|
||||
'1.0.0',
|
||||
versionedSiteDir,
|
||||
DEFAULT_PLUGIN_ID,
|
||||
|
@ -132,7 +167,12 @@ describe('docsVersion', () => {
|
|||
test('no docs file to version', () => {
|
||||
const emptySiteDir = path.join(fixtureDir, 'empty-site');
|
||||
expect(() =>
|
||||
docsVersion('1.0.0', emptySiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
|
||||
cliDocsVersionCommand(
|
||||
'1.0.0',
|
||||
emptySiteDir,
|
||||
DEFAULT_PLUGIN_ID,
|
||||
DEFAULT_OPTIONS,
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[docs] There is no docs to version !"`,
|
||||
);
|
||||
|
@ -159,23 +199,23 @@ describe('docsVersion', () => {
|
|||
path: 'docs',
|
||||
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(
|
||||
path.join(simpleSiteDir, options.path),
|
||||
path.join(
|
||||
getVersionedDocsDir(simpleSiteDir, DEFAULT_PLUGIN_ID),
|
||||
getVersionedDocsDirPath(simpleSiteDir, DEFAULT_PLUGIN_ID),
|
||||
'version-1.0.0',
|
||||
),
|
||||
);
|
||||
expect(versionedSidebar).toMatchSnapshot();
|
||||
expect(versionedSidebarPath).toEqual(
|
||||
path.join(
|
||||
getVersionedSidebarsDir(simpleSiteDir, DEFAULT_PLUGIN_ID),
|
||||
getVersionedSidebarsDirPath(simpleSiteDir, DEFAULT_PLUGIN_ID),
|
||||
'version-1.0.0-sidebars.json',
|
||||
),
|
||||
);
|
||||
expect(versionsPath).toEqual(
|
||||
getVersionsJSONFile(simpleSiteDir, DEFAULT_PLUGIN_ID),
|
||||
getVersionsFilePath(simpleSiteDir, DEFAULT_PLUGIN_ID),
|
||||
);
|
||||
expect(versions).toEqual(['1.0.0']);
|
||||
expect(consoleMock).toHaveBeenCalledWith('[docs] Version 1.0.0 created!');
|
||||
|
@ -207,23 +247,28 @@ describe('docsVersion', () => {
|
|||
path: 'docs',
|
||||
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(
|
||||
path.join(versionedSiteDir, options.path),
|
||||
path.join(
|
||||
getVersionedDocsDir(versionedSiteDir, DEFAULT_PLUGIN_ID),
|
||||
getVersionedDocsDirPath(versionedSiteDir, DEFAULT_PLUGIN_ID),
|
||||
'version-2.0.0',
|
||||
),
|
||||
);
|
||||
expect(versionedSidebar).toMatchSnapshot();
|
||||
expect(versionedSidebarPath).toEqual(
|
||||
path.join(
|
||||
getVersionedSidebarsDir(versionedSiteDir, DEFAULT_PLUGIN_ID),
|
||||
getVersionedSidebarsDirPath(versionedSiteDir, DEFAULT_PLUGIN_ID),
|
||||
'version-2.0.0-sidebars.json',
|
||||
),
|
||||
);
|
||||
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(consoleMock).toHaveBeenCalledWith('[docs] Version 2.0.0 created!');
|
||||
|
@ -257,23 +302,23 @@ describe('docsVersion', () => {
|
|||
path: 'community',
|
||||
sidebarPath: path.join(versionedSiteDir, 'community_sidebars.json'),
|
||||
};
|
||||
docsVersion('2.0.0', versionedSiteDir, pluginId, options);
|
||||
cliDocsVersionCommand('2.0.0', versionedSiteDir, pluginId, options);
|
||||
expect(copyMock).toHaveBeenCalledWith(
|
||||
path.join(versionedSiteDir, options.path),
|
||||
path.join(
|
||||
getVersionedDocsDir(versionedSiteDir, pluginId),
|
||||
getVersionedDocsDirPath(versionedSiteDir, pluginId),
|
||||
'version-2.0.0',
|
||||
),
|
||||
);
|
||||
expect(versionedSidebar).toMatchSnapshot();
|
||||
expect(versionedSidebarPath).toEqual(
|
||||
path.join(
|
||||
getVersionedSidebarsDir(versionedSiteDir, pluginId),
|
||||
getVersionedSidebarsDirPath(versionedSiteDir, pluginId),
|
||||
'version-2.0.0-sidebars.json',
|
||||
),
|
||||
);
|
||||
expect(versionsPath).toEqual(
|
||||
getVersionsJSONFile(versionedSiteDir, pluginId),
|
||||
getVersionsFilePath(versionedSiteDir, pluginId),
|
||||
);
|
||||
expect(versions).toEqual(['2.0.0', '1.0.0']);
|
||||
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 {validate} from 'webpack';
|
||||
import {isMatch} from 'picomatch';
|
||||
import commander from 'commander';
|
||||
import {kebabCase} from 'lodash';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import pluginContentDocs from '../index';
|
||||
import loadEnv from '../env';
|
||||
import {loadContext} from '@docusaurus/core/src/server/index';
|
||||
import {applyConfigureWebpack} from '@docusaurus/core/src/webpack/utils';
|
||||
import {RouteConfig} from '@docusaurus/types';
|
||||
|
@ -19,9 +19,26 @@ import {posixPath} from '@docusaurus/utils';
|
|||
import {sortConfig} from '@docusaurus/core/src/server/plugins';
|
||||
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
|
||||
|
||||
import * as version from '../version';
|
||||
import {PluginOptionSchema} from '../pluginOptionSchema';
|
||||
import * as cliDocs from '../cli';
|
||||
import {OptionsSchema} from '../options';
|
||||
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 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!
|
||||
const utils = {
|
||||
getGlobalData: () => globalDataContainer,
|
||||
getRouteConfigs: () => routeConfigs,
|
||||
// query by prefix, because files have a hash at the end
|
||||
// so it's not convenient to query by full filename
|
||||
getCreatedDataByPrefix: (prefix: string) => {
|
||||
const entry = Object.entries(dataContainer).find(([key]) =>
|
||||
key.startsWith(prefix),
|
||||
|
||||
checkVersionMetadataPropCreated: (version: LoadedVersion) => {
|
||||
const versionMetadataProp = getCreatedDataByPrefix(
|
||||
`version-${kebabCase(version.versionName)}-metadata-prop`,
|
||||
);
|
||||
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: () => {
|
||||
|
@ -79,11 +110,11 @@ test('site with wrong sidebar file', async () => {
|
|||
const sidebarPath = path.join(siteDir, 'wrong-sidebars.json');
|
||||
const plugin = pluginContentDocs(
|
||||
context,
|
||||
normalizePluginOptions(PluginOptionSchema, {
|
||||
normalizePluginOptions(OptionsSchema, {
|
||||
sidebarPath,
|
||||
}),
|
||||
);
|
||||
await expect(plugin.loadContent()).rejects.toThrowErrorMatchingSnapshot();
|
||||
await expect(plugin.loadContent!()).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
describe('empty/no docs website', () => {
|
||||
|
@ -94,33 +125,26 @@ describe('empty/no docs website', () => {
|
|||
await fs.ensureDir(path.join(siteDir, 'docs'));
|
||||
const plugin = pluginContentDocs(
|
||||
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 () => {
|
||||
const plugin = pluginContentDocs(
|
||||
context,
|
||||
normalizePluginOptions(PluginOptionSchema, {
|
||||
path: '/path/does/not/exist/',
|
||||
}),
|
||||
expect(() =>
|
||||
pluginContentDocs(
|
||||
context,
|
||||
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 context = loadContext(siteDir);
|
||||
const sidebarPath = path.join(siteDir, 'sidebars.json');
|
||||
const pluginPath = 'docs';
|
||||
const plugin = pluginContentDocs(
|
||||
context,
|
||||
normalizePluginOptions(PluginOptionSchema, {
|
||||
path: pluginPath,
|
||||
normalizePluginOptions(OptionsSchema, {
|
||||
path: 'docs',
|
||||
sidebarPath,
|
||||
homePageId: 'hello',
|
||||
}),
|
||||
|
@ -140,27 +163,31 @@ describe('simple website', () => {
|
|||
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
|
||||
|
||||
test('extendCli - docsVersion', () => {
|
||||
const mock = jest.spyOn(version, 'docsVersion').mockImplementation();
|
||||
const mock = jest
|
||||
.spyOn(cliDocs, 'cliDocsVersionCommand')
|
||||
.mockImplementation();
|
||||
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']);
|
||||
expect(mock).toHaveBeenCalledTimes(1);
|
||||
expect(mock).toHaveBeenCalledWith('1.0.0', siteDir, DEFAULT_PLUGIN_ID, {
|
||||
path: pluginPath,
|
||||
path: 'docs',
|
||||
sidebarPath,
|
||||
});
|
||||
mock.mockRestore();
|
||||
});
|
||||
|
||||
test('getPathToWatch', () => {
|
||||
const pathToWatch = plugin.getPathsToWatch();
|
||||
const pathToWatch = plugin.getPathsToWatch!();
|
||||
const matchPattern = pathToWatch.map((filepath) =>
|
||||
posixPath(path.relative(siteDir, filepath)),
|
||||
);
|
||||
expect(matchPattern).not.toEqual([]);
|
||||
expect(matchPattern).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"docs/**/*.{md,mdx}",
|
||||
"sidebars.json",
|
||||
"docs/**/*.{md,mdx}",
|
||||
]
|
||||
`);
|
||||
expect(isMatch('docs/hello.md', matchPattern)).toEqual(true);
|
||||
|
@ -192,15 +219,13 @@ describe('simple website', () => {
|
|||
});
|
||||
|
||||
test('content', async () => {
|
||||
const content = await plugin.loadContent();
|
||||
const {
|
||||
docsMetadata,
|
||||
docsSidebars,
|
||||
versionToSidebars,
|
||||
permalinkToSidebar,
|
||||
} = content;
|
||||
expect(versionToSidebars).toEqual({});
|
||||
expect(docsMetadata.hello).toEqual({
|
||||
const content = await plugin.loadContent!();
|
||||
expect(content.loadedVersions.length).toEqual(1);
|
||||
const [currentVersion] = content.loadedVersions;
|
||||
|
||||
expect(findDocById(currentVersion, 'hello')).toEqual({
|
||||
...defaultDocMetadata,
|
||||
version: 'current',
|
||||
id: 'hello',
|
||||
unversionedId: 'hello',
|
||||
isDocsHomePage: true,
|
||||
|
@ -211,12 +236,18 @@ describe('simple website', () => {
|
|||
permalink: '/docs/foo/bazSlug.html',
|
||||
},
|
||||
sidebar: 'docs',
|
||||
source: path.join('@site', pluginPath, 'hello.md'),
|
||||
source: path.join(
|
||||
'@site',
|
||||
path.relative(siteDir, currentVersion.docsDirPath),
|
||||
'hello.md',
|
||||
),
|
||||
title: 'Hello, World !',
|
||||
description: 'Hi, Endilie here :)',
|
||||
});
|
||||
|
||||
expect(docsMetadata['foo/bar']).toEqual({
|
||||
expect(findDocById(currentVersion, 'foo/bar')).toEqual({
|
||||
...defaultDocMetadata,
|
||||
version: 'current',
|
||||
id: 'foo/bar',
|
||||
unversionedId: 'foo/bar',
|
||||
isDocsHomePage: false,
|
||||
|
@ -227,26 +258,30 @@ describe('simple website', () => {
|
|||
permalink: '/docs/foo/bar',
|
||||
slug: '/foo/bar',
|
||||
sidebar: 'docs',
|
||||
source: path.join('@site', pluginPath, 'foo', 'bar.md'),
|
||||
source: path.join(
|
||||
'@site',
|
||||
path.relative(siteDir, currentVersion.docsDirPath),
|
||||
'foo',
|
||||
'bar.md',
|
||||
),
|
||||
title: 'Bar',
|
||||
description: 'This is custom description',
|
||||
});
|
||||
|
||||
expect(docsSidebars).toMatchSnapshot();
|
||||
expect(currentVersion.sidebars).toMatchSnapshot();
|
||||
|
||||
const {actions, utils} = createFakeActions(pluginContentDir);
|
||||
|
||||
await plugin.contentLoaded({
|
||||
await plugin.contentLoaded!({
|
||||
content,
|
||||
actions,
|
||||
allContent: {},
|
||||
});
|
||||
|
||||
// There is only one nested docs route for simple site
|
||||
const baseMetadata = utils.getCreatedDataByPrefix('docs-route-');
|
||||
expect(baseMetadata.docsSidebars).toEqual(docsSidebars);
|
||||
expect(baseMetadata.permalinkToSidebar).toEqual(permalinkToSidebar);
|
||||
utils.checkVersionMetadataPropCreated(currentVersion);
|
||||
|
||||
utils.expectSnapshot();
|
||||
|
||||
expect(utils.getGlobalData()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
@ -258,25 +293,24 @@ describe('versioned website', () => {
|
|||
const routeBasePath = 'docs';
|
||||
const plugin = pluginContentDocs(
|
||||
context,
|
||||
normalizePluginOptions(PluginOptionSchema, {
|
||||
normalizePluginOptions(OptionsSchema, {
|
||||
routeBasePath,
|
||||
sidebarPath,
|
||||
homePageId: 'hello',
|
||||
}),
|
||||
);
|
||||
const env = loadEnv(siteDir, DEFAULT_PLUGIN_ID);
|
||||
const {docsDir: versionedDir} = env.versioning;
|
||||
|
||||
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
|
||||
|
||||
test('isVersioned', () => {
|
||||
expect(env.versioning.enabled).toEqual(true);
|
||||
});
|
||||
|
||||
test('extendCli - docsVersion', () => {
|
||||
const mock = jest.spyOn(version, 'docsVersion').mockImplementation();
|
||||
const mock = jest
|
||||
.spyOn(cliDocs, 'cliDocsVersionCommand')
|
||||
.mockImplementation();
|
||||
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']);
|
||||
expect(mock).toHaveBeenCalledTimes(1);
|
||||
expect(mock).toHaveBeenCalledWith('2.0.0', siteDir, DEFAULT_PLUGIN_ID, {
|
||||
path: routeBasePath,
|
||||
sidebarPath,
|
||||
|
@ -285,21 +319,21 @@ describe('versioned website', () => {
|
|||
});
|
||||
|
||||
test('getPathToWatch', () => {
|
||||
const pathToWatch = plugin.getPathsToWatch();
|
||||
const pathToWatch = plugin.getPathsToWatch!();
|
||||
const matchPattern = pathToWatch.map((filepath) =>
|
||||
posixPath(path.relative(siteDir, filepath)),
|
||||
);
|
||||
expect(matchPattern).not.toEqual([]);
|
||||
expect(matchPattern).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"sidebars.json",
|
||||
"docs/**/*.{md,mdx}",
|
||||
"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_sidebars/version-1.0.0-sidebars.json",
|
||||
"versioned_docs/version-1.0.0/**/*.{md,mdx}",
|
||||
"versioned_sidebars/version-withSlugs-sidebars.json",
|
||||
"versioned_docs/version-withSlugs/**/*.{md,mdx}",
|
||||
"sidebars.json",
|
||||
]
|
||||
`);
|
||||
expect(isMatch('docs/hello.md', matchPattern)).toEqual(true);
|
||||
|
@ -335,50 +369,65 @@ describe('versioned website', () => {
|
|||
});
|
||||
|
||||
test('content', async () => {
|
||||
const content = await plugin.loadContent();
|
||||
const {
|
||||
docsMetadata,
|
||||
docsSidebars,
|
||||
versionToSidebars,
|
||||
permalinkToSidebar,
|
||||
} = content;
|
||||
const content = await plugin.loadContent!();
|
||||
expect(content.loadedVersions.length).toEqual(4);
|
||||
const [
|
||||
currentVersion,
|
||||
version101,
|
||||
version100,
|
||||
versionWithSlugs,
|
||||
] = content.loadedVersions;
|
||||
|
||||
// foo/baz.md only exists in version -1.0.0
|
||||
expect(docsMetadata['foo/baz']).toBeUndefined();
|
||||
expect(docsMetadata['version-1.0.1/foo/baz']).toBeUndefined();
|
||||
expect(docsMetadata['foo/bar']).toEqual({
|
||||
expect(findDocById(currentVersion, 'foo/baz')).toBeUndefined();
|
||||
expect(findDocById(version101, 'foo/baz')).toBeUndefined();
|
||||
expect(findDocById(versionWithSlugs, 'foo/baz')).toBeUndefined();
|
||||
|
||||
expect(findDocById(currentVersion, 'foo/bar')).toEqual({
|
||||
...defaultDocMetadata,
|
||||
id: 'foo/bar',
|
||||
unversionedId: 'foo/bar',
|
||||
isDocsHomePage: false,
|
||||
permalink: '/docs/next/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',
|
||||
description: 'This is next version of bar.',
|
||||
version: 'next',
|
||||
version: 'current',
|
||||
sidebar: 'docs',
|
||||
next: {
|
||||
title: 'hello',
|
||||
permalink: '/docs/next/',
|
||||
},
|
||||
});
|
||||
expect(docsMetadata.hello).toEqual({
|
||||
expect(findDocById(currentVersion, 'hello')).toEqual({
|
||||
...defaultDocMetadata,
|
||||
id: 'hello',
|
||||
unversionedId: 'hello',
|
||||
isDocsHomePage: true,
|
||||
permalink: '/docs/next/',
|
||||
slug: '/',
|
||||
source: path.join('@site', routeBasePath, 'hello.md'),
|
||||
source: path.join(
|
||||
'@site',
|
||||
path.relative(siteDir, currentVersion.docsDirPath),
|
||||
'hello.md',
|
||||
),
|
||||
title: 'hello',
|
||||
description: 'Hello next !',
|
||||
version: 'next',
|
||||
version: 'current',
|
||||
sidebar: 'docs',
|
||||
previous: {
|
||||
title: 'bar',
|
||||
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',
|
||||
unversionedId: 'hello',
|
||||
isDocsHomePage: true,
|
||||
|
@ -386,8 +435,7 @@ describe('versioned website', () => {
|
|||
slug: '/',
|
||||
source: path.join(
|
||||
'@site',
|
||||
path.relative(siteDir, versionedDir),
|
||||
'version-1.0.1',
|
||||
path.relative(siteDir, version101.docsDirPath),
|
||||
'hello.md',
|
||||
),
|
||||
title: 'hello',
|
||||
|
@ -399,7 +447,8 @@ describe('versioned website', () => {
|
|||
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',
|
||||
unversionedId: 'foo/baz',
|
||||
isDocsHomePage: false,
|
||||
|
@ -407,8 +456,7 @@ describe('versioned website', () => {
|
|||
slug: '/foo/baz',
|
||||
source: path.join(
|
||||
'@site',
|
||||
path.relative(siteDir, versionedDir),
|
||||
'version-1.0.0',
|
||||
path.relative(siteDir, version100.docsDirPath),
|
||||
'foo',
|
||||
'baz.md',
|
||||
),
|
||||
|
@ -427,47 +475,24 @@ describe('versioned website', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(docsSidebars).toMatchSnapshot('all sidebars');
|
||||
expect(versionToSidebars).toMatchSnapshot(
|
||||
'sidebars needed for each version',
|
||||
expect(currentVersion.sidebars).toMatchSnapshot('current version sidebars');
|
||||
expect(version101.sidebars).toMatchSnapshot('101 version sidebars');
|
||||
expect(version100.sidebars).toMatchSnapshot('100 version sidebars');
|
||||
expect(versionWithSlugs.sidebars).toMatchSnapshot(
|
||||
'withSlugs version sidebars',
|
||||
);
|
||||
|
||||
const {actions, utils} = createFakeActions(pluginContentDir);
|
||||
await plugin.contentLoaded({
|
||||
await plugin.contentLoaded!({
|
||||
content,
|
||||
actions,
|
||||
allContent: {},
|
||||
});
|
||||
|
||||
// The created base metadata for each nested docs route is smartly chunked/ splitted across version
|
||||
const latestVersionBaseMetadata = utils.getCreatedDataByPrefix(
|
||||
'docs-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(
|
||||
'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.checkVersionMetadataPropCreated(currentVersion);
|
||||
utils.checkVersionMetadataPropCreated(version101);
|
||||
utils.checkVersionMetadataPropCreated(version100);
|
||||
utils.checkVersionMetadataPropCreated(versionWithSlugs);
|
||||
|
||||
utils.expectSnapshot();
|
||||
});
|
||||
|
@ -481,26 +506,24 @@ describe('versioned website (community)', () => {
|
|||
const pluginId = 'community';
|
||||
const plugin = pluginContentDocs(
|
||||
context,
|
||||
normalizePluginOptions(PluginOptionSchema, {
|
||||
normalizePluginOptions(OptionsSchema, {
|
||||
id: 'community',
|
||||
path: 'community',
|
||||
routeBasePath,
|
||||
sidebarPath,
|
||||
}),
|
||||
);
|
||||
const env = loadEnv(siteDir, pluginId);
|
||||
const {docsDir: versionedDir} = env.versioning;
|
||||
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
|
||||
|
||||
test('isVersioned', () => {
|
||||
expect(env.versioning.enabled).toEqual(true);
|
||||
});
|
||||
|
||||
test('extendCli - docsVersion', () => {
|
||||
const mock = jest.spyOn(version, 'docsVersion').mockImplementation();
|
||||
const mock = jest
|
||||
.spyOn(cliDocs, 'cliDocsVersionCommand')
|
||||
.mockImplementation();
|
||||
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']);
|
||||
expect(mock).toHaveBeenCalledTimes(1);
|
||||
expect(mock).toHaveBeenCalledWith('2.0.0', siteDir, pluginId, {
|
||||
path: routeBasePath,
|
||||
sidebarPath,
|
||||
|
@ -509,17 +532,17 @@ describe('versioned website (community)', () => {
|
|||
});
|
||||
|
||||
test('getPathToWatch', () => {
|
||||
const pathToWatch = plugin.getPathsToWatch();
|
||||
const pathToWatch = plugin.getPathsToWatch!();
|
||||
const matchPattern = pathToWatch.map((filepath) =>
|
||||
posixPath(path.relative(siteDir, filepath)),
|
||||
);
|
||||
expect(matchPattern).not.toEqual([]);
|
||||
expect(matchPattern).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"community_sidebars.json",
|
||||
"community/**/*.{md,mdx}",
|
||||
"community_versioned_sidebars/version-1.0.0-sidebars.json",
|
||||
"community_versioned_docs/version-1.0.0/**/*.{md,mdx}",
|
||||
"community_sidebars.json",
|
||||
]
|
||||
`);
|
||||
expect(isMatch('community/team.md', matchPattern)).toEqual(true);
|
||||
|
@ -545,27 +568,29 @@ describe('versioned website (community)', () => {
|
|||
});
|
||||
|
||||
test('content', async () => {
|
||||
const content = await plugin.loadContent();
|
||||
const {
|
||||
docsMetadata,
|
||||
docsSidebars,
|
||||
versionToSidebars,
|
||||
permalinkToSidebar,
|
||||
} = content;
|
||||
const content = await plugin.loadContent!();
|
||||
expect(content.loadedVersions.length).toEqual(2);
|
||||
const [currentVersion, version100] = content.loadedVersions;
|
||||
|
||||
expect(docsMetadata.team).toEqual({
|
||||
expect(findDocById(currentVersion, 'team')).toEqual({
|
||||
...defaultDocMetadata,
|
||||
id: 'team',
|
||||
unversionedId: 'team',
|
||||
isDocsHomePage: false,
|
||||
permalink: '/community/next/team',
|
||||
slug: '/team',
|
||||
source: path.join('@site', routeBasePath, 'team.md'),
|
||||
source: path.join(
|
||||
'@site',
|
||||
path.relative(siteDir, currentVersion.docsDirPath),
|
||||
'team.md',
|
||||
),
|
||||
title: 'team',
|
||||
description: 'Team current version',
|
||||
version: 'next',
|
||||
version: 'current',
|
||||
sidebar: 'community',
|
||||
});
|
||||
expect(docsMetadata['version-1.0.0/team']).toEqual({
|
||||
expect(findDocById(version100, 'team')).toEqual({
|
||||
...defaultDocMetadata,
|
||||
id: 'version-1.0.0/team',
|
||||
unversionedId: 'team',
|
||||
isDocsHomePage: false,
|
||||
|
@ -573,8 +598,7 @@ describe('versioned website (community)', () => {
|
|||
slug: '/team',
|
||||
source: path.join(
|
||||
'@site',
|
||||
path.relative(siteDir, versionedDir),
|
||||
'version-1.0.0',
|
||||
path.relative(siteDir, version100.docsDirPath),
|
||||
'team.md',
|
||||
),
|
||||
title: 'team',
|
||||
|
@ -583,38 +607,18 @@ describe('versioned website (community)', () => {
|
|||
sidebar: 'version-1.0.0/community',
|
||||
});
|
||||
|
||||
expect(docsSidebars).toMatchSnapshot('all sidebars');
|
||||
expect(versionToSidebars).toMatchSnapshot(
|
||||
'sidebars needed for each version',
|
||||
);
|
||||
expect(currentVersion.sidebars).toMatchSnapshot('current version sidebars');
|
||||
expect(version100.sidebars).toMatchSnapshot('100 version sidebars');
|
||||
|
||||
const {actions, utils} = createFakeActions(pluginContentDir);
|
||||
await plugin.contentLoaded({
|
||||
await plugin.contentLoaded!({
|
||||
content,
|
||||
actions,
|
||||
allContent: {},
|
||||
});
|
||||
|
||||
// The created base metadata for each nested docs route is smartly chunked/ splitted across version
|
||||
const latestVersionBaseMetadata = utils.getCreatedDataByPrefix(
|
||||
'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.checkVersionMetadataPropCreated(currentVersion);
|
||||
utils.checkVersionMetadataPropCreated(version100);
|
||||
|
||||
utils.expectSnapshot();
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@ import fs from 'fs';
|
|||
import path from 'path';
|
||||
import shell from 'shelljs';
|
||||
|
||||
import lastUpdate from '../lastUpdate';
|
||||
import {getFileLastUpdate} from '../lastUpdate';
|
||||
|
||||
describe('lastUpdate', () => {
|
||||
const existingFilePath = path.join(
|
||||
|
@ -17,7 +17,7 @@ describe('lastUpdate', () => {
|
|||
'__fixtures__/simple-site/docs/hello.md',
|
||||
);
|
||||
test('existing test file in repository with Git timestamp', async () => {
|
||||
const lastUpdateData = await lastUpdate(existingFilePath);
|
||||
const lastUpdateData = await getFileLastUpdate(existingFilePath);
|
||||
expect(lastUpdateData).not.toBeNull();
|
||||
|
||||
const {author, timestamp} = lastUpdateData;
|
||||
|
@ -36,29 +36,29 @@ describe('lastUpdate', () => {
|
|||
'__fixtures__',
|
||||
'.nonExisting',
|
||||
);
|
||||
expect(await lastUpdate(nonExistingFilePath)).toBeNull();
|
||||
expect(await getFileLastUpdate(nonExistingFilePath)).toBeNull();
|
||||
expect(consoleMock).toHaveBeenCalledTimes(1);
|
||||
expect(consoleMock).toHaveBeenCalledWith(
|
||||
new Error(
|
||||
`Command failed with exit code 128: git log -1 --format=%ct, %an ${nonExistingFilePath}`,
|
||||
),
|
||||
);
|
||||
expect(await lastUpdate(null)).toBeNull();
|
||||
expect(await lastUpdate(undefined)).toBeNull();
|
||||
expect(await getFileLastUpdate(null)).toBeNull();
|
||||
expect(await getFileLastUpdate(undefined)).toBeNull();
|
||||
consoleMock.mockRestore();
|
||||
});
|
||||
|
||||
test('temporary created file that has no git timestamp', async () => {
|
||||
const tempFilePath = path.join(__dirname, '__fixtures__', '.temp');
|
||||
fs.writeFileSync(tempFilePath, 'Lorem ipsum :)');
|
||||
expect(await lastUpdate(tempFilePath)).toBeNull();
|
||||
expect(await getFileLastUpdate(tempFilePath)).toBeNull();
|
||||
fs.unlinkSync(tempFilePath);
|
||||
});
|
||||
|
||||
test('Git does not exist', async () => {
|
||||
const mock = jest.spyOn(shell, 'which').mockImplementationOnce(() => null);
|
||||
const consoleMock = jest.spyOn(console, 'warn').mockImplementation();
|
||||
const lastUpdateData = await lastUpdate(existingFilePath);
|
||||
const lastUpdateData = await getFileLastUpdate(existingFilePath);
|
||||
expect(lastUpdateData).toBeNull();
|
||||
expect(consoleMock).toHaveBeenLastCalledWith(
|
||||
'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.
|
||||
*/
|
||||
|
||||
import {PluginOptionSchema, DEFAULT_OPTIONS} from '../pluginOptionSchema';
|
||||
import {OptionsSchema, DEFAULT_OPTIONS} from '../options';
|
||||
import {normalizePluginOptions} from '@docusaurus/utils-validation';
|
||||
|
||||
// the type of remark/rehype plugins is function
|
||||
|
@ -14,7 +14,7 @@ const markdownPluginsObjectStub = {};
|
|||
|
||||
describe('normalizeDocsPluginOptions', () => {
|
||||
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(error).toBe(undefined);
|
||||
});
|
||||
|
@ -34,9 +34,10 @@ describe('normalizeDocsPluginOptions', () => {
|
|||
showLastUpdateAuthor: true,
|
||||
admonitions: {},
|
||||
excludeNextVersionDocs: true,
|
||||
includeCurrentVersion: false,
|
||||
disableVersioning: true,
|
||||
};
|
||||
const {value, error} = await PluginOptionSchema.validate(userOptions);
|
||||
const {value, error} = await OptionsSchema.validate(userOptions);
|
||||
expect(value).toEqual(userOptions);
|
||||
expect(error).toBe(undefined);
|
||||
});
|
||||
|
@ -50,14 +51,14 @@ describe('normalizeDocsPluginOptions', () => {
|
|||
[markdownPluginsFunctionStub, {option1: '42'}],
|
||||
],
|
||||
};
|
||||
const {value, error} = await PluginOptionSchema.validate(userOptions);
|
||||
const {value, error} = await OptionsSchema.validate(userOptions);
|
||||
expect(value).toEqual(userOptions);
|
||||
expect(error).toBe(undefined);
|
||||
});
|
||||
|
||||
test('should reject invalid remark plugin options', () => {
|
||||
expect(() => {
|
||||
normalizePluginOptions(PluginOptionSchema, {
|
||||
normalizePluginOptions(OptionsSchema, {
|
||||
remarkPlugins: [[{option1: '42'}, markdownPluginsFunctionStub]],
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
|
@ -67,7 +68,7 @@ describe('normalizeDocsPluginOptions', () => {
|
|||
|
||||
test('should reject invalid rehype plugin options', () => {
|
||||
expect(() => {
|
||||
normalizePluginOptions(PluginOptionSchema, {
|
||||
normalizePluginOptions(OptionsSchema, {
|
||||
rehypePlugins: [
|
||||
[
|
||||
markdownPluginsFunctionStub,
|
||||
|
@ -83,7 +84,7 @@ describe('normalizeDocsPluginOptions', () => {
|
|||
|
||||
test('should reject bad path inputs', () => {
|
||||
expect(() => {
|
||||
normalizePluginOptions(PluginOptionSchema, {
|
||||
normalizePluginOptions(OptionsSchema, {
|
||||
path: 2,
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"\\"path\\" must be a string"`);
|
||||
|
@ -91,7 +92,7 @@ describe('normalizeDocsPluginOptions', () => {
|
|||
|
||||
test('should reject bad include inputs', () => {
|
||||
expect(() => {
|
||||
normalizePluginOptions(PluginOptionSchema, {
|
||||
normalizePluginOptions(OptionsSchema, {
|
||||
include: '**/*.{md,mdx}',
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"\\"include\\" must be an array"`);
|
||||
|
@ -99,7 +100,7 @@ describe('normalizeDocsPluginOptions', () => {
|
|||
|
||||
test('should reject bad showLastUpdateTime inputs', () => {
|
||||
expect(() => {
|
||||
normalizePluginOptions(PluginOptionSchema, {
|
||||
normalizePluginOptions(OptionsSchema, {
|
||||
showLastUpdateTime: 'true',
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
|
@ -109,7 +110,7 @@ describe('normalizeDocsPluginOptions', () => {
|
|||
|
||||
test('should reject bad remarkPlugins input', () => {
|
||||
expect(() => {
|
||||
normalizePluginOptions(PluginOptionSchema, {
|
||||
normalizePluginOptions(OptionsSchema, {
|
||||
remarkPlugins: 'remark-math',
|
||||
});
|
||||
}).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 loadSidebars from '../sidebars';
|
||||
import {
|
||||
loadSidebars,
|
||||
collectSidebarDocItems,
|
||||
collectSidebarsDocIds,
|
||||
createSidebarsUtils,
|
||||
} from '../sidebars';
|
||||
import {Sidebar, Sidebars} from '../types';
|
||||
|
||||
/* eslint-disable global-require, import/no-dynamic-require */
|
||||
|
||||
|
@ -14,13 +20,13 @@ describe('loadSidebars', () => {
|
|||
const fixtureDir = path.join(__dirname, '__fixtures__', 'sidebars');
|
||||
test('sidebars with known sidebar item type', async () => {
|
||||
const sidebarPath = path.join(fixtureDir, 'sidebars.json');
|
||||
const result = loadSidebars([sidebarPath]);
|
||||
const result = loadSidebars(sidebarPath);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('sidebars with deep level of category', async () => {
|
||||
const sidebarPath = path.join(fixtureDir, 'sidebars-category.js');
|
||||
const result = loadSidebars([sidebarPath]);
|
||||
const result = loadSidebars(sidebarPath);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
@ -30,8 +36,8 @@ describe('loadSidebars', () => {
|
|||
fixtureDir,
|
||||
'sidebars-category-shorthand.js',
|
||||
);
|
||||
const sidebar1 = loadSidebars([sidebarPath1]);
|
||||
const sidebar2 = loadSidebars([sidebarPath2]);
|
||||
const sidebar1 = loadSidebars(sidebarPath1);
|
||||
const sidebar2 = loadSidebars(sidebarPath2);
|
||||
expect(sidebar1).toEqual(sidebar2);
|
||||
});
|
||||
|
||||
|
@ -40,9 +46,7 @@ describe('loadSidebars', () => {
|
|||
fixtureDir,
|
||||
'sidebars-category-wrong-items.json',
|
||||
);
|
||||
expect(() =>
|
||||
loadSidebars([sidebarPath]),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error loading {\\"type\\":\\"category\\",\\"label\\":\\"Category Label\\",\\"items\\":\\"doc1\\"}. \\"items\\" must be an array."`,
|
||||
);
|
||||
});
|
||||
|
@ -52,9 +56,7 @@ describe('loadSidebars', () => {
|
|||
fixtureDir,
|
||||
'sidebars-category-wrong-label.json',
|
||||
);
|
||||
expect(() =>
|
||||
loadSidebars([sidebarPath]),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error loading {\\"type\\":\\"category\\",\\"label\\":true,\\"items\\":[\\"doc1\\"]}. \\"label\\" must be a string."`,
|
||||
);
|
||||
});
|
||||
|
@ -64,9 +66,7 @@ describe('loadSidebars', () => {
|
|||
fixtureDir,
|
||||
'sidebars-doc-id-not-string.json',
|
||||
);
|
||||
expect(() =>
|
||||
loadSidebars([sidebarPath]),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error loading {\\"type\\":\\"doc\\",\\"id\\":[\\"doc1\\"]}. \\"id\\" must be a string."`,
|
||||
);
|
||||
});
|
||||
|
@ -76,60 +76,75 @@ describe('loadSidebars', () => {
|
|||
fixtureDir,
|
||||
'sidebars-first-level-not-category.js',
|
||||
);
|
||||
const result = loadSidebars([sidebarPath]);
|
||||
const result = loadSidebars(sidebarPath);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('sidebars link', async () => {
|
||||
const sidebarPath = path.join(fixtureDir, 'sidebars-link.json');
|
||||
const result = loadSidebars([sidebarPath]);
|
||||
const result = loadSidebars(sidebarPath);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('sidebars link wrong label', async () => {
|
||||
const sidebarPath = path.join(fixtureDir, 'sidebars-link-wrong-label.json');
|
||||
expect(() =>
|
||||
loadSidebars([sidebarPath]),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error loading {\\"type\\":\\"link\\",\\"label\\":false,\\"href\\":\\"https://github.com\\"}. \\"label\\" must be a string."`,
|
||||
);
|
||||
});
|
||||
|
||||
test('sidebars link wrong href', async () => {
|
||||
const sidebarPath = path.join(fixtureDir, 'sidebars-link-wrong-href.json');
|
||||
expect(() =>
|
||||
loadSidebars([sidebarPath]),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error loading {\\"type\\":\\"link\\",\\"label\\":\\"GitHub\\",\\"href\\":[\\"example.com\\"]}. \\"href\\" must be a string."`,
|
||||
);
|
||||
});
|
||||
|
||||
test('sidebars with unknown sidebar item type', async () => {
|
||||
const sidebarPath = path.join(fixtureDir, 'sidebars-unknown-type.json');
|
||||
expect(() =>
|
||||
loadSidebars([sidebarPath]),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unknown sidebar item type [superman]. Sidebar item={\\"type\\":\\"superman\\"} "`,
|
||||
);
|
||||
});
|
||||
|
||||
test('sidebars with known sidebar item type but wrong field', async () => {
|
||||
const sidebarPath = path.join(fixtureDir, 'sidebars-wrong-field.json');
|
||||
expect(() =>
|
||||
loadSidebars([sidebarPath]),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unknown sidebar item keys: href. Item: {\\"type\\":\\"category\\",\\"label\\":\\"category\\",\\"href\\":\\"https://github.com\\"}"`,
|
||||
);
|
||||
});
|
||||
|
||||
test('no sidebars', () => {
|
||||
const result = loadSidebars(null);
|
||||
expect(result).toEqual({});
|
||||
test('unexisting path', () => {
|
||||
expect(() => loadSidebars('badpath')).toThrowErrorMatchingInlineSnapshot(
|
||||
`"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 () => {
|
||||
const sidebarPath = path.join(fixtureDir, 'sidebars-collapsed.json');
|
||||
const result = loadSidebars([sidebarPath]);
|
||||
const result = loadSidebars(sidebarPath);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
@ -138,7 +153,177 @@ describe('loadSidebars', () => {
|
|||
fixtureDir,
|
||||
'sidebars-collapsed-first-level.json',
|
||||
);
|
||||
const result = loadSidebars([sidebarPath]);
|
||||
const result = loadSidebars(sidebarPath);
|
||||
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 {
|
||||
getVersionsJSONFile,
|
||||
getVersionedDocsDir,
|
||||
getVersionedSidebarsDir,
|
||||
} from './env';
|
||||
getVersionsFilePath,
|
||||
getVersionedDocsDirPath,
|
||||
getVersionedSidebarsDirPath,
|
||||
} from './versions';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import {Sidebar, PathOptions, SidebarItem} from './types';
|
||||
import loadSidebars from './sidebars';
|
||||
import {Sidebars, PathOptions, SidebarItem} from './types';
|
||||
import {loadSidebars} from './sidebars';
|
||||
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
|
||||
|
||||
// Tests depend on non-default export for mocking.
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export function docsVersion(
|
||||
export function cliDocsVersionCommand(
|
||||
version: string | null | undefined,
|
||||
siteDir: string,
|
||||
pluginId: string,
|
||||
|
@ -63,7 +63,7 @@ export function docsVersion(
|
|||
|
||||
// Load existing versions.
|
||||
let versions = [];
|
||||
const versionsJSONFile = getVersionsJSONFile(siteDir, pluginId);
|
||||
const versionsJSONFile = getVersionsFilePath(siteDir, pluginId);
|
||||
if (fs.existsSync(versionsJSONFile)) {
|
||||
versions = JSON.parse(fs.readFileSync(versionsJSONFile, 'utf8'));
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ export function docsVersion(
|
|||
// Copy docs files.
|
||||
const docsDir = path.join(siteDir, docsPath);
|
||||
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}`);
|
||||
fs.copySync(docsDir, newVersionDir);
|
||||
} else {
|
||||
|
@ -89,7 +89,7 @@ export function docsVersion(
|
|||
|
||||
// Load current sidebar and create a new versioned sidebars file.
|
||||
if (fs.existsSync(sidebarPath)) {
|
||||
const loadedSidebars: Sidebar = loadSidebars([sidebarPath]);
|
||||
const loadedSidebars: Sidebars = loadSidebars(sidebarPath);
|
||||
|
||||
// Transform id in original sidebar to versioned id.
|
||||
const normalizeItem = (item: SidebarItem): SidebarItem => {
|
||||
|
@ -107,8 +107,8 @@ export function docsVersion(
|
|||
}
|
||||
};
|
||||
|
||||
const versionedSidebar: Sidebar = Object.entries(loadedSidebars).reduce(
|
||||
(acc: Sidebar, [sidebarId, sidebarItems]) => {
|
||||
const versionedSidebar: Sidebars = Object.entries(loadedSidebars).reduce(
|
||||
(acc: Sidebars, [sidebarId, sidebarItems]) => {
|
||||
const newVersionedSidebarId = `version-${version}/${sidebarId}`;
|
||||
acc[newVersionedSidebarId] = sidebarItems.map(normalizeItem);
|
||||
return acc;
|
||||
|
@ -116,7 +116,7 @@ export function docsVersion(
|
|||
{},
|
||||
);
|
||||
|
||||
const versionedSidebarsDir = getVersionedSidebarsDir(siteDir, pluginId);
|
||||
const versionedSidebarsDir = getVersionedSidebarsDirPath(siteDir, pluginId);
|
||||
const newSidebarFile = path.join(
|
||||
versionedSidebarsDir,
|
||||
`version-${version}-sidebars.json`,
|
|
@ -12,7 +12,7 @@ import {
|
|||
getActiveDocContext,
|
||||
getActiveVersion,
|
||||
getDocVersionSuggestions,
|
||||
} from '../../client/docsClientUtils';
|
||||
} from '../docsClientUtils';
|
||||
import {GlobalPluginData, GlobalVersion} from '../../types';
|
||||
import {shuffle} from 'lodash';
|
||||
|
||||
|
@ -21,12 +21,10 @@ describe('docsClientUtils', () => {
|
|||
const data: Record<string, GlobalPluginData> = {
|
||||
pluginIosId: {
|
||||
path: '/ios',
|
||||
latestVersionName: 'xyz',
|
||||
versions: [],
|
||||
},
|
||||
pluginAndroidId: {
|
||||
path: '/android',
|
||||
latestVersionName: 'xyz',
|
||||
versions: [],
|
||||
},
|
||||
};
|
||||
|
@ -55,19 +53,25 @@ describe('docsClientUtils', () => {
|
|||
const versions: GlobalVersion[] = [
|
||||
{
|
||||
name: 'version1',
|
||||
label: 'version1',
|
||||
path: '/???',
|
||||
isLast: false,
|
||||
docs: [],
|
||||
mainDocId: '???',
|
||||
},
|
||||
{
|
||||
name: 'version2',
|
||||
label: 'version2',
|
||||
path: '/???',
|
||||
isLast: true,
|
||||
docs: [],
|
||||
mainDocId: '???',
|
||||
},
|
||||
{
|
||||
name: 'version3',
|
||||
label: 'version3',
|
||||
path: '/???',
|
||||
isLast: false,
|
||||
docs: [],
|
||||
mainDocId: '???',
|
||||
},
|
||||
|
@ -76,52 +80,35 @@ describe('docsClientUtils', () => {
|
|||
expect(
|
||||
getLatestVersion({
|
||||
path: '???',
|
||||
latestVersionName: 'does not exist',
|
||||
versions,
|
||||
}),
|
||||
).toEqual(undefined);
|
||||
expect(
|
||||
getLatestVersion({
|
||||
path: '???',
|
||||
latestVersionName: 'version1',
|
||||
versions,
|
||||
})?.name,
|
||||
).toEqual('version1');
|
||||
expect(
|
||||
getLatestVersion({
|
||||
path: '???',
|
||||
latestVersionName: 'version2',
|
||||
versions,
|
||||
})?.name,
|
||||
).toEqual('version2');
|
||||
expect(
|
||||
getLatestVersion({
|
||||
path: '???',
|
||||
latestVersionName: 'version3',
|
||||
versions,
|
||||
})?.name,
|
||||
).toEqual('version3');
|
||||
).toEqual(versions[1]);
|
||||
});
|
||||
|
||||
test('getActiveVersion', () => {
|
||||
const data: GlobalPluginData = {
|
||||
path: 'docs',
|
||||
latestVersionName: 'version2',
|
||||
versions: [
|
||||
{
|
||||
name: 'next',
|
||||
label: 'next',
|
||||
isLast: false,
|
||||
path: '/docs/next',
|
||||
docs: [],
|
||||
mainDocId: '???',
|
||||
},
|
||||
{
|
||||
name: 'version2',
|
||||
label: 'version2',
|
||||
isLast: true,
|
||||
path: '/docs',
|
||||
docs: [],
|
||||
mainDocId: '???',
|
||||
},
|
||||
{
|
||||
name: 'version1',
|
||||
label: 'version1',
|
||||
isLast: false,
|
||||
path: '/docs/version1',
|
||||
docs: [],
|
||||
mainDocId: '???',
|
||||
|
@ -146,7 +133,9 @@ describe('docsClientUtils', () => {
|
|||
test('getActiveDocContext', () => {
|
||||
const versionNext: GlobalVersion = {
|
||||
name: 'next',
|
||||
label: 'next',
|
||||
path: '/docs/next',
|
||||
isLast: false,
|
||||
mainDocId: 'doc1',
|
||||
docs: [
|
||||
{
|
||||
|
@ -162,6 +151,8 @@ describe('docsClientUtils', () => {
|
|||
|
||||
const version2: GlobalVersion = {
|
||||
name: 'version2',
|
||||
label: 'version2',
|
||||
isLast: true,
|
||||
path: '/docs',
|
||||
mainDocId: 'doc1',
|
||||
docs: [
|
||||
|
@ -178,7 +169,9 @@ describe('docsClientUtils', () => {
|
|||
|
||||
const version1: GlobalVersion = {
|
||||
name: 'version1',
|
||||
label: 'version1',
|
||||
path: '/docs/version1',
|
||||
isLast: false,
|
||||
mainDocId: 'doc1',
|
||||
docs: [
|
||||
{
|
||||
|
@ -197,7 +190,6 @@ describe('docsClientUtils', () => {
|
|||
|
||||
const data: GlobalPluginData = {
|
||||
path: 'docs',
|
||||
latestVersionName: 'version2',
|
||||
versions,
|
||||
};
|
||||
|
||||
|
@ -270,6 +262,8 @@ describe('docsClientUtils', () => {
|
|||
test('getDocVersionSuggestions', () => {
|
||||
const versionNext: GlobalVersion = {
|
||||
name: 'next',
|
||||
label: 'next',
|
||||
isLast: false,
|
||||
path: '/docs/next',
|
||||
mainDocId: 'doc1',
|
||||
docs: [
|
||||
|
@ -286,7 +280,9 @@ describe('docsClientUtils', () => {
|
|||
|
||||
const version2: GlobalVersion = {
|
||||
name: 'version2',
|
||||
label: 'version2',
|
||||
path: '/docs',
|
||||
isLast: true,
|
||||
mainDocId: 'doc1',
|
||||
docs: [
|
||||
{
|
||||
|
@ -302,6 +298,8 @@ describe('docsClientUtils', () => {
|
|||
|
||||
const version1: GlobalVersion = {
|
||||
name: 'version1',
|
||||
label: 'version1',
|
||||
isLast: false,
|
||||
path: '/docs/version1',
|
||||
mainDocId: 'doc1',
|
||||
docs: [
|
||||
|
@ -321,7 +319,6 @@ describe('docsClientUtils', () => {
|
|||
|
||||
const data: GlobalPluginData = {
|
||||
path: 'docs',
|
||||
latestVersionName: 'version2',
|
||||
versions,
|
||||
};
|
||||
|
|
@ -49,9 +49,7 @@ export type ActiveDocContext = {
|
|||
};
|
||||
|
||||
export const getLatestVersion = (data: GlobalPluginData): Version => {
|
||||
return data.versions.find(
|
||||
(version) => version.name === data.latestVersionName,
|
||||
)!;
|
||||
return data.versions.find((version) => version.isLast)!;
|
||||
};
|
||||
|
||||
// Note: return undefined on doc-unrelated pages,
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
* 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_SIDEBARS_DIR = 'versioned_sidebars';
|
||||
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.
|
||||
*/
|
||||
|
||||
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 chalk from 'chalk';
|
||||
|
||||
import admonitions from 'remark-admonitions';
|
||||
import {
|
||||
STATIC_DIR_NAME,
|
||||
DEFAULT_PLUGIN_ID,
|
||||
} from '@docusaurus/core/lib/constants';
|
||||
import {
|
||||
normalizeUrl,
|
||||
docuHash,
|
||||
objectWithKeySorted,
|
||||
aliasedSitePath,
|
||||
} from '@docusaurus/utils';
|
||||
import {
|
||||
LoadContext,
|
||||
Plugin,
|
||||
RouteConfig,
|
||||
OptionValidationContext,
|
||||
ValidationResult,
|
||||
} from '@docusaurus/types';
|
||||
import {normalizeUrl, docuHash, aliasedSitePath} from '@docusaurus/utils';
|
||||
import {LoadContext, Plugin, RouteConfig} from '@docusaurus/types';
|
||||
|
||||
import createOrder from './order';
|
||||
import loadSidebars from './sidebars';
|
||||
import processMetadata from './metadata';
|
||||
import loadEnv from './env';
|
||||
import {loadSidebars, createSidebarsUtils} from './sidebars';
|
||||
import {readVersionDocs, processDocMetadata} from './docs';
|
||||
import {readVersionsMetadata} from './versions';
|
||||
|
||||
import {
|
||||
PluginOptions,
|
||||
Sidebar,
|
||||
Order,
|
||||
DocsMetadata,
|
||||
LoadedContent,
|
||||
SourceToPermalink,
|
||||
PermalinkToSidebar,
|
||||
SidebarItemLink,
|
||||
SidebarItemDoc,
|
||||
DocsSidebar,
|
||||
DocsBaseMetadata,
|
||||
MetadataRaw,
|
||||
DocsMetadataRaw,
|
||||
Metadata,
|
||||
VersionToSidebars,
|
||||
SidebarItem,
|
||||
DocsSidebarItem,
|
||||
DocMetadataBase,
|
||||
DocMetadata,
|
||||
GlobalPluginData,
|
||||
DocsVersion,
|
||||
GlobalVersion,
|
||||
GlobalDoc,
|
||||
VersionMetadata,
|
||||
DocNavLink,
|
||||
LoadedVersion,
|
||||
DocFile,
|
||||
DocsMarkdownOption,
|
||||
} from './types';
|
||||
import {Configuration} from 'webpack';
|
||||
import {docsVersion} from './version';
|
||||
import {RuleSetRule} from 'webpack';
|
||||
import {cliDocsVersionCommand} from './cli';
|
||||
import {VERSIONS_JSON_FILE} from './constants';
|
||||
import {PluginOptionSchema} from './pluginOptionSchema';
|
||||
import {ValidationError} from '@hapi/joi';
|
||||
import {OptionsSchema} from './options';
|
||||
import {flatten, keyBy, compact} from 'lodash';
|
||||
import {toGlobalDataVersion} from './globalData';
|
||||
import {toVersionMetadataProp} from './props';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export default function pluginContentDocs(
|
||||
context: LoadContext,
|
||||
options: PluginOptions,
|
||||
): Plugin<LoadedContent | null, typeof PluginOptionSchema> {
|
||||
// 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],
|
||||
]);
|
||||
}
|
||||
|
||||
): Plugin<LoadedContent, typeof OptionsSchema> {
|
||||
const {siteDir, generatedFilesDir, baseUrl} = context;
|
||||
const docsDir = path.resolve(siteDir, options.path);
|
||||
|
||||
const versionsMetadata = readVersionsMetadata({context, options});
|
||||
|
||||
const sourceToPermalink: SourceToPermalink = {};
|
||||
const pluginId = options.id ?? DEFAULT_PLUGIN_ID;
|
||||
const isDefaultPluginId = pluginId === DEFAULT_PLUGIN_ID;
|
||||
|
||||
const pluginDataDirRoot = path.join(
|
||||
generatedFilesDir,
|
||||
|
@ -101,18 +60,6 @@ export default function pluginContentDocs(
|
|||
const aliasedSource = (source: string) =>
|
||||
`~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 {
|
||||
name: 'docusaurus-plugin-content-docs',
|
||||
|
||||
|
@ -125,6 +72,10 @@ export default function pluginContentDocs(
|
|||
},
|
||||
|
||||
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
|
||||
? 'docs:version'
|
||||
: `docs:version:${pluginId}`;
|
||||
|
@ -137,259 +88,159 @@ export default function pluginContentDocs(
|
|||
.arguments('<version>')
|
||||
.description(commandDescription)
|
||||
.action((version) => {
|
||||
docsVersion(version, siteDir, pluginId, {
|
||||
cliDocsVersionCommand(version, siteDir, pluginId, {
|
||||
path: options.path,
|
||||
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() {
|
||||
const modules = [];
|
||||
|
||||
if (options.admonitions) {
|
||||
modules.push(require.resolve('remark-admonitions/styles/infima.css'));
|
||||
}
|
||||
|
||||
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() {
|
||||
const {include, sidebarPath} = options;
|
||||
|
||||
if (!fs.existsSync(docsDir)) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`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) {
|
||||
async function loadVersionDocsBase(
|
||||
versionMetadata: VersionMetadata,
|
||||
): Promise<DocMetadataBase[]> {
|
||||
const docFiles = await readVersionDocs(versionMetadata, options);
|
||||
if (docFiles.length === 0) {
|
||||
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(docsMetadataRaw).sort().join('\n- ')}`,
|
||||
`Docs version ${
|
||||
versionMetadata.versionName
|
||||
} 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 {
|
||||
type: 'link',
|
||||
label: sidebar_label || title,
|
||||
href: permalink,
|
||||
...versionMetadata,
|
||||
mainDocId: getMainDoc().unversionedId,
|
||||
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 {
|
||||
docsMetadata,
|
||||
docsDir,
|
||||
docsSidebars,
|
||||
permalinkToSidebar: objectWithKeySorted(permalinkToSidebar),
|
||||
versionToSidebars,
|
||||
loadedVersions: await Promise.all(versionsMetadata.map(loadVersion)),
|
||||
};
|
||||
},
|
||||
|
||||
async contentLoaded({content, actions}) {
|
||||
if (!content || Object.keys(content.docsMetadata).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {docLayoutComponent, docItemComponent, routeBasePath} = options;
|
||||
const {loadedVersions} = content;
|
||||
const {docLayoutComponent, docItemComponent} = options;
|
||||
const {addRoute, createData, setGlobalData} = actions;
|
||||
|
||||
const pluginInstanceGlobalData: GlobalPluginData = {
|
||||
path: normalizeUrl([baseUrl, options.routeBasePath]),
|
||||
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[],
|
||||
const createDocRoutes = async (
|
||||
docs: DocMetadata[],
|
||||
): Promise<RouteConfig[]> => {
|
||||
const routes = await Promise.all(
|
||||
metadataItems.map(async (metadataItem) => {
|
||||
docs.map(async (metadataItem) => {
|
||||
await createData(
|
||||
// Note that this created data path must be in sync with
|
||||
// metadataPath provided to mdx-loader.
|
||||
|
@ -411,111 +262,84 @@ Available document ids=
|
|||
return routes.sort((a, b) => a.path.localeCompare(b.path));
|
||||
};
|
||||
|
||||
// We want latest version route to have lower priority
|
||||
// Otherwise `/docs/next/foo` would match
|
||||
// `/docs/:route` instead of `/docs/next/:route`.
|
||||
const getVersionRoutePriority = (version: DocsVersion) =>
|
||||
version === versioning.latestVersion ? -1 : undefined;
|
||||
|
||||
// This is the base route of the document root (for a doc given version)
|
||||
// (/docs, /docs/next, /docs/1.0 etc...)
|
||||
// The component applies the layout and renders the appropriate doc
|
||||
const addVersionRoute = async (
|
||||
docsBasePath: string,
|
||||
docsBaseMetadata: DocsBaseMetadata,
|
||||
docs: Metadata[],
|
||||
priority?: number,
|
||||
) => {
|
||||
const docsBaseMetadataPath = await createData(
|
||||
`${docuHash(normalizeUrl([docsBasePath, ':route']))}.json`,
|
||||
JSON.stringify(docsBaseMetadata, null, 2),
|
||||
async function handleVersion(loadedVersion: LoadedVersion) {
|
||||
const versionMetadataPropPath = await createData(
|
||||
`${docuHash(
|
||||
`version-${loadedVersion.versionName}-metadata-prop`,
|
||||
)}.json`,
|
||||
JSON.stringify(toVersionMetadataProp(loadedVersion), 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({
|
||||
path: docsBasePath,
|
||||
exact: false, // allow matching /docs/* as well
|
||||
component: docLayoutComponent, // main docs component (DocPage)
|
||||
routes: docsRoutes, // subroute for each doc
|
||||
path: loadedVersion.versionPath,
|
||||
// allow matching /docs/* as well
|
||||
exact: false,
|
||||
// main docs component (DocPage)
|
||||
component: docLayoutComponent,
|
||||
// sub-routes for each doc
|
||||
routes: await createDocRoutes(loadedVersion.docs),
|
||||
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)
|
||||
pluginInstanceGlobalData.versions = sortBy(
|
||||
pluginInstanceGlobalData.versions,
|
||||
(versionMetadata: GlobalVersion) => {
|
||||
const orderedVersionNames = ['next', ...versions];
|
||||
return orderedVersionNames.indexOf(versionMetadata.name!);
|
||||
},
|
||||
);
|
||||
await Promise.all(loadedVersions.map(handleVersion));
|
||||
|
||||
setGlobalData<GlobalPluginData>({
|
||||
path: normalizeUrl([baseUrl, options.routeBasePath]),
|
||||
versions: loadedVersions.map(toGlobalDataVersion),
|
||||
});
|
||||
},
|
||||
|
||||
configureWebpack(_config, isServer, utils) {
|
||||
const {getBabelLoader, getCacheLoader} = utils;
|
||||
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.
|
||||
const stats = {
|
||||
warningsFilter: [VERSIONS_JSON_FILE],
|
||||
|
@ -532,55 +356,11 @@ Available document ids=
|
|||
},
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
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),
|
||||
},
|
||||
],
|
||||
rules: [createMDXLoaderRule()],
|
||||
},
|
||||
} as Configuration;
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function validateOptions({
|
||||
validate,
|
||||
options,
|
||||
}: OptionValidationContext<PluginOptions, ValidationError>): ValidationResult<
|
||||
PluginOptions,
|
||||
ValidationError
|
||||
> {
|
||||
const validatedOptions = validate(PluginOptionSchema, options);
|
||||
return validatedOptions;
|
||||
}
|
||||
export {validateOptions} from './options';
|
||||
|
|
|
@ -14,7 +14,7 @@ const GIT_COMMIT_TIMESTAMP_AUTHOR_REGEX = /^(\d+), (.+)$/;
|
|||
|
||||
let showedGitRequirementError = false;
|
||||
|
||||
export default async function getFileLastUpdate(
|
||||
export async function getFileLastUpdate(
|
||||
filePath?: string,
|
||||
): Promise<FileLastUpdateData | null> {
|
||||
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 path from 'path';
|
||||
import linkify from '../linkify';
|
||||
import {SourceToPermalink} from '../../types';
|
||||
import {VERSIONED_DOCS_DIR} from '../../constants';
|
||||
import {linkify} from '../linkify';
|
||||
import {
|
||||
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 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 = {
|
||||
'@site/docs/doc1.md': '/docs/doc1',
|
||||
'@site/docs/doc2.md': '/docs/doc2',
|
||||
|
@ -24,28 +52,34 @@ const sourceToPermalink: SourceToPermalink = {
|
|||
'/docs/1.0.0/subdir/doc1',
|
||||
};
|
||||
|
||||
const transform = (filepath) => {
|
||||
const content = fs.readFileSync(filepath, 'utf-8');
|
||||
const transformedContent = linkify(
|
||||
content,
|
||||
filepath,
|
||||
docsDir,
|
||||
siteDir,
|
||||
function createMarkdownOptions(
|
||||
options?: Partial<DocsMarkdownOption>,
|
||||
): DocsMarkdownOption {
|
||||
return {
|
||||
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];
|
||||
};
|
||||
|
||||
test('transform nothing', () => {
|
||||
const doc1 = path.join(docsDir, 'doc1.md');
|
||||
const doc1 = path.join(versionCurrent.docsDirPath, 'doc1.md');
|
||||
const [content, transformedContent] = transform(doc1);
|
||||
expect(transformedContent).toMatchSnapshot();
|
||||
expect(content).toEqual(transformedContent);
|
||||
});
|
||||
|
||||
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);
|
||||
expect(transformedContent).toMatchSnapshot();
|
||||
expect(transformedContent).toContain('](/docs/doc1');
|
||||
|
@ -58,7 +92,8 @@ test('transform to correct 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);
|
||||
expect(transformedContent).toMatchSnapshot();
|
||||
expect(transformedContent).toContain('](/docs/doc2');
|
||||
|
@ -67,7 +102,7 @@ test('transform relative 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);
|
||||
expect(transformedContent).toMatchSnapshot();
|
||||
expect(transformedContent).toContain('[doc1]: /docs/doc1');
|
||||
|
@ -77,8 +112,38 @@ test('transforms reference links', () => {
|
|||
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', () => {
|
||||
const doc2 = path.join(versionedDir, 'version-1.0.0', 'doc2.md');
|
||||
const doc2 = path.join(version100.docsDirPath, 'doc2.md');
|
||||
const [content, transformedContent] = transform(doc2);
|
||||
expect(transformedContent).toMatchSnapshot();
|
||||
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', () => {
|
||||
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);
|
||||
expect(transformedContent).toMatchSnapshot();
|
||||
expect(transformedContent).toContain('](/docs/1.0.0/doc2');
|
||||
|
|
|
@ -7,26 +7,15 @@
|
|||
|
||||
import {getOptions} from 'loader-utils';
|
||||
import {loader} from 'webpack';
|
||||
import linkify from './linkify';
|
||||
import {linkify} from './linkify';
|
||||
import {DocsMarkdownOption} from '../types';
|
||||
|
||||
const markdownLoader: loader.Loader = function (source) {
|
||||
const fileString = source as string;
|
||||
const callback = this.async();
|
||||
const {docsDir, siteDir, versionedDir, sourceToPermalink} = getOptions(this);
|
||||
|
||||
const options = getOptions(this) as DocsMarkdownOption;
|
||||
return (
|
||||
callback &&
|
||||
callback(
|
||||
null,
|
||||
linkify(
|
||||
fileString,
|
||||
this.resourcePath,
|
||||
docsDir,
|
||||
siteDir,
|
||||
sourceToPermalink,
|
||||
versionedDir,
|
||||
),
|
||||
)
|
||||
callback && callback(null, linkify(fileString, this.resourcePath, options))
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -7,68 +7,81 @@
|
|||
|
||||
import path from 'path';
|
||||
import {resolve} from 'url';
|
||||
import {getSubFolder} from '@docusaurus/utils';
|
||||
import {SourceToPermalink} from '../types';
|
||||
import {
|
||||
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,
|
||||
filePath: string,
|
||||
docsDir: string,
|
||||
siteDir: string,
|
||||
sourceToPermalink: SourceToPermalink,
|
||||
versionedDir?: string,
|
||||
): string {
|
||||
// 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;
|
||||
version: VersionMetadata,
|
||||
options: DocsMarkdownOption,
|
||||
) {
|
||||
const {siteDir, sourceToPermalink, onBrokenMarkdownLink} = options;
|
||||
const {docsDirPath} = version;
|
||||
|
||||
// Replace internal markdown linking (except in fenced blocks).
|
||||
if (sourceDir) {
|
||||
let fencedBlock = false;
|
||||
const lines = content.split('\n').map((line) => {
|
||||
if (line.trim().startsWith('```')) {
|
||||
fencedBlock = !fencedBlock;
|
||||
}
|
||||
if (fencedBlock) {
|
||||
return line;
|
||||
}
|
||||
let fencedBlock = false;
|
||||
const lines = fileString.split('\n').map((line) => {
|
||||
if (line.trim().startsWith('```')) {
|
||||
fencedBlock = !fencedBlock;
|
||||
}
|
||||
if (fencedBlock) {
|
||||
return line;
|
||||
}
|
||||
|
||||
let modifiedLine = line;
|
||||
// Replace inline-style links or reference-style links e.g:
|
||||
// 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
|
||||
const mdRegex = /(?:(?:\]\()|(?:\]:\s?))(?!https)([^'")\]\s>]+\.mdx?)/g;
|
||||
let mdMatch = mdRegex.exec(modifiedLine);
|
||||
while (mdMatch !== null) {
|
||||
// Replace it to correct html link.
|
||||
const mdLink = mdMatch[1];
|
||||
const targetSource = `${sourceDir}/${mdLink}`;
|
||||
const aliasedSource = (source: string) =>
|
||||
`@site/${path.relative(siteDir, source)}`;
|
||||
const permalink =
|
||||
sourceToPermalink[aliasedSource(resolve(thisSource, mdLink))] ||
|
||||
sourceToPermalink[aliasedSource(targetSource)];
|
||||
if (permalink) {
|
||||
modifiedLine = modifiedLine.replace(mdLink, permalink);
|
||||
}
|
||||
mdMatch = mdRegex.exec(modifiedLine);
|
||||
let modifiedLine = line;
|
||||
// Replace inline-style links or reference-style links e.g:
|
||||
// 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
|
||||
const mdRegex = /(?:(?:\]\()|(?:\]:\s?))(?!https)([^'")\]\s>]+\.mdx?)/g;
|
||||
let mdMatch = mdRegex.exec(modifiedLine);
|
||||
while (mdMatch !== null) {
|
||||
// Replace it to correct html link.
|
||||
const mdLink = mdMatch[1];
|
||||
const targetSource = `${docsDirPath}/${mdLink}`;
|
||||
const aliasedSource = (source: string) =>
|
||||
`@site/${path.relative(siteDir, source)}`;
|
||||
const permalink =
|
||||
sourceToPermalink[aliasedSource(resolve(filePath, mdLink))] ||
|
||||
sourceToPermalink[aliasedSource(targetSource)];
|
||||
if (permalink) {
|
||||
modifiedLine = modifiedLine.replace(mdLink, permalink);
|
||||
} else {
|
||||
const brokenMarkdownLink: BrokenMarkdownLink = {
|
||||
version,
|
||||
filePath,
|
||||
link: mdLink,
|
||||
};
|
||||
onBrokenMarkdownLink(brokenMarkdownLink);
|
||||
}
|
||||
return modifiedLine;
|
||||
});
|
||||
mdMatch = mdRegex.exec(modifiedLine);
|
||||
}
|
||||
return modifiedLine;
|
||||
});
|
||||
|
||||
content = lines.join('\n');
|
||||
}
|
||||
|
||||
return content;
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
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,
|
||||
URISchema,
|
||||
} 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.
|
||||
routeBasePath: 'docs', // URL Route.
|
||||
homePageId: undefined, // TODO remove soon, deprecated
|
||||
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',
|
||||
docItemComponent: '@theme/DocItem',
|
||||
remarkPlugins: [],
|
||||
|
@ -27,10 +31,11 @@ export const DEFAULT_OPTIONS: PluginOptions = {
|
|||
showLastUpdateAuthor: false,
|
||||
admonitions: {},
|
||||
excludeNextVersionDocs: false,
|
||||
includeCurrentVersion: true,
|
||||
disableVersioning: false,
|
||||
};
|
||||
|
||||
export const PluginOptionSchema = Joi.object({
|
||||
export const OptionsSchema = Joi.object({
|
||||
path: Joi.string().default(DEFAULT_OPTIONS.path),
|
||||
editUrl: URISchema,
|
||||
routeBasePath: Joi.string().allow('').default(DEFAULT_OPTIONS.routeBasePath),
|
||||
|
@ -49,5 +54,50 @@ export const PluginOptionSchema = Joi.object({
|
|||
excludeNextVersionDocs: Joi.bool().default(
|
||||
DEFAULT_OPTIONS.excludeNextVersionDocs,
|
||||
),
|
||||
includeCurrentVersion: Joi.bool().default(
|
||||
DEFAULT_OPTIONS.includeCurrentVersion,
|
||||
),
|
||||
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 importFresh from 'import-fresh';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarRaw,
|
||||
Sidebars,
|
||||
SidebarItem,
|
||||
SidebarItemCategoryRaw,
|
||||
SidebarItemRaw,
|
||||
SidebarItemLink,
|
||||
SidebarItemDoc,
|
||||
SidebarCategoryShorthandRaw,
|
||||
Sidebar,
|
||||
} 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(
|
||||
item: SidebarItemRaw,
|
||||
): item is SidebarCategoryShorthandRaw {
|
||||
item: SidebarItemJSON,
|
||||
): item is SidebarCategoryShorthandJSON {
|
||||
return typeof item !== 'string' && !item.type;
|
||||
}
|
||||
|
||||
|
@ -32,8 +60,8 @@ const defaultCategoryCollapsedValue = true;
|
|||
* Convert {category1: [item1,item2]} shorthand syntax to long-form syntax
|
||||
*/
|
||||
function normalizeCategoryShorthand(
|
||||
sidebar: SidebarCategoryShorthandRaw,
|
||||
): SidebarItemCategoryRaw[] {
|
||||
sidebar: SidebarCategoryShorthandJSON,
|
||||
): SidebarItemCategoryJSON[] {
|
||||
return Object.entries(sidebar).map(([label, items]) => ({
|
||||
type: 'category',
|
||||
collapsed: defaultCategoryCollapsedValue,
|
||||
|
@ -65,7 +93,7 @@ function assertItem<K extends string>(
|
|||
|
||||
function assertIsCategory(
|
||||
item: unknown,
|
||||
): asserts item is SidebarItemCategoryRaw {
|
||||
): asserts item is SidebarItemCategoryJSON {
|
||||
assertItem(item, ['items', 'label', 'collapsed']);
|
||||
if (typeof item.label !== 'string') {
|
||||
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
|
||||
* each item will be an object with the corresponding type.
|
||||
*/
|
||||
function normalizeItem(item: SidebarItemRaw): SidebarItem[] {
|
||||
function normalizeItem(item: SidebarItemJSON): SidebarItem[] {
|
||||
if (typeof item === 'string') {
|
||||
return [
|
||||
{
|
||||
|
@ -155,38 +183,119 @@ function normalizeItem(item: SidebarItemRaw): SidebarItem[] {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts sidebars object to mapping to arrays of sidebar item objects.
|
||||
*/
|
||||
function normalizeSidebar(sidebars: SidebarRaw): Sidebar {
|
||||
return Object.entries(sidebars).reduce(
|
||||
(acc: Sidebar, [sidebarId, sidebar]) => {
|
||||
const normalizedSidebar: SidebarItemRaw[] = Array.isArray(sidebar)
|
||||
? sidebar
|
||||
: normalizeCategoryShorthand(sidebar);
|
||||
function normalizeSidebar(sidebar: SidebarJSON) {
|
||||
const normalizedSidebar: SidebarItemJSON[] = Array.isArray(sidebar)
|
||||
? sidebar
|
||||
: normalizeCategoryShorthand(sidebar);
|
||||
|
||||
acc[sidebarId] = flatMap(normalizedSidebar, normalizeItem);
|
||||
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
return flatMap(normalizedSidebar, normalizeItem);
|
||||
}
|
||||
|
||||
export default function loadSidebars(sidebarPaths?: string[]): Sidebar {
|
||||
// We don't want sidebars to be cached because of hot reloading.
|
||||
const allSidebars: SidebarRaw = {};
|
||||
function normalizeSidebars(sidebars: SidebarsJSON): Sidebars {
|
||||
return mapValues(sidebars, normalizeSidebar);
|
||||
}
|
||||
|
||||
if (!sidebarPaths || !sidebarPaths.length) {
|
||||
return {} as Sidebar;
|
||||
// TODO refactor: make async
|
||||
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) => {
|
||||
if (sidebarPath && fs.existsSync(sidebarPath)) {
|
||||
const sidebar = importFresh(sidebarPath) as SidebarRaw;
|
||||
Object.assign(allSidebars, sidebar);
|
||||
}
|
||||
});
|
||||
|
||||
return normalizeSidebar(allSidebars);
|
||||
return flatten(sidebar.map(collectRecursive));
|
||||
}
|
||||
|
||||
export function collectSidebarsDocIds(
|
||||
sidebars: Sidebars,
|
||||
): Record<string, string[]> {
|
||||
return mapValues(sidebars, (sidebar) => {
|
||||
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 = [];
|
||||
}
|
||||
|
||||
// TODO deprecate in favor of useDocs.ts instead
|
||||
function useVersioning(): {
|
||||
versioningEnabled: boolean;
|
||||
versions: string[];
|
||||
|
|
|
@ -8,116 +8,91 @@
|
|||
// eslint-disable-next-line spaced-comment
|
||||
/// <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;
|
||||
homePageId?: string;
|
||||
editUrl?: string;
|
||||
showLastUpdateTime?: boolean;
|
||||
showLastUpdateAuthor?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export interface PathOptions {
|
||||
export type PathOptions = {
|
||||
path: string;
|
||||
sidebarPath: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface PluginOptions extends MetadataOptions, PathOptions {
|
||||
id?: string;
|
||||
include: string[];
|
||||
docLayoutComponent: string;
|
||||
docItemComponent: string;
|
||||
remarkPlugins: ([Function, object] | Function)[];
|
||||
rehypePlugins: string[];
|
||||
admonitions: any;
|
||||
disableVersioning: boolean;
|
||||
excludeNextVersionDocs: boolean;
|
||||
}
|
||||
export type PluginOptions = MetadataOptions &
|
||||
PathOptions & {
|
||||
id: string;
|
||||
include: string[];
|
||||
docLayoutComponent: string;
|
||||
docItemComponent: string;
|
||||
remarkPlugins: ([Function, object] | Function)[];
|
||||
rehypePlugins: string[];
|
||||
admonitions: any;
|
||||
disableVersioning: boolean;
|
||||
excludeNextVersionDocs?: boolean;
|
||||
includeCurrentVersion: boolean;
|
||||
};
|
||||
|
||||
export type SidebarItemDoc = {
|
||||
type: 'doc' | 'ref';
|
||||
id: string;
|
||||
};
|
||||
|
||||
export interface SidebarItemLink {
|
||||
export type SidebarItemLink = {
|
||||
type: 'link';
|
||||
href: string;
|
||||
label: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface SidebarItemCategory {
|
||||
export type SidebarItemCategory = {
|
||||
type: 'category';
|
||||
label: string;
|
||||
items: SidebarItem[];
|
||||
collapsed: boolean;
|
||||
}
|
||||
|
||||
export interface SidebarItemCategoryRaw {
|
||||
type: 'category';
|
||||
label: string;
|
||||
items: SidebarItemRaw[];
|
||||
collapsed?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export type SidebarItem =
|
||||
| SidebarItemDoc
|
||||
| SidebarItemLink
|
||||
| SidebarItemCategory;
|
||||
|
||||
export type SidebarItemRaw =
|
||||
| string
|
||||
| SidebarCategoryShorthandRaw
|
||||
| SidebarItemDoc
|
||||
| SidebarItemLink
|
||||
| SidebarItemCategoryRaw
|
||||
| {
|
||||
type: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
export type Sidebar = SidebarItem[];
|
||||
|
||||
export interface SidebarCategoryShorthandRaw {
|
||||
[sidebarCategory: string]: SidebarItemRaw[];
|
||||
}
|
||||
export type Sidebars = Record<string, Sidebar>;
|
||||
|
||||
// Sidebar given by user that is not normalized yet. e.g: sidebars.json
|
||||
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 {
|
||||
export type OrderMetadata = {
|
||||
previous?: string;
|
||||
next?: string;
|
||||
sidebar?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Order {
|
||||
[id: string]: OrderMetadata;
|
||||
}
|
||||
|
||||
export interface LastUpdateData {
|
||||
export type LastUpdateData = {
|
||||
lastUpdatedAt?: number;
|
||||
lastUpdatedBy?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface MetadataRaw extends LastUpdateData {
|
||||
export type DocMetadataBase = LastUpdateData & {
|
||||
version: VersionName;
|
||||
unversionedId: string;
|
||||
id: string;
|
||||
isDocsHomePage: boolean;
|
||||
|
@ -128,67 +103,38 @@ export interface MetadataRaw extends LastUpdateData {
|
|||
permalink: string;
|
||||
sidebar_label?: string;
|
||||
editUrl?: string | null;
|
||||
version?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Paginator {
|
||||
export type DocNavLink = {
|
||||
title: string;
|
||||
permalink: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Metadata extends MetadataRaw {
|
||||
export type DocMetadata = DocMetadataBase & {
|
||||
sidebar?: string;
|
||||
previous?: Paginator;
|
||||
next?: Paginator;
|
||||
}
|
||||
previous?: DocNavLink;
|
||||
next?: DocNavLink;
|
||||
};
|
||||
|
||||
export interface DocsMetadata {
|
||||
[id: string]: Metadata;
|
||||
}
|
||||
|
||||
export interface DocsMetadataRaw {
|
||||
[id: string]: MetadataRaw;
|
||||
}
|
||||
|
||||
export interface SourceToPermalink {
|
||||
export type SourceToPermalink = {
|
||||
[source: string]: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface PermalinkToSidebar {
|
||||
export type PermalinkToSidebar = {
|
||||
[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 = {
|
||||
enabled: boolean;
|
||||
latestVersion: string | null;
|
||||
versions: string[];
|
||||
docsDir: string;
|
||||
sidebarsDir: string;
|
||||
export type LoadedVersion = VersionMetadata & {
|
||||
versionPath: string;
|
||||
mainDocId: string;
|
||||
docs: DocMetadata[];
|
||||
sidebars: Sidebars;
|
||||
permalinkToSidebar: Record<string, string>;
|
||||
};
|
||||
|
||||
export interface Env {
|
||||
versioning: VersioningEnv;
|
||||
// TODO: translation
|
||||
}
|
||||
export type LoadedContent = {
|
||||
loadedVersions: LoadedVersion[];
|
||||
};
|
||||
|
||||
export type GlobalDoc = {
|
||||
id: string;
|
||||
|
@ -196,7 +142,9 @@ export type GlobalDoc = {
|
|||
};
|
||||
|
||||
export type GlobalVersion = {
|
||||
name: DocsVersion;
|
||||
name: VersionName;
|
||||
label: string;
|
||||
isLast: boolean;
|
||||
path: string;
|
||||
mainDocId: string; // home doc (if docs homepage configured), or first doc
|
||||
docs: GlobalDoc[];
|
||||
|
@ -204,6 +152,39 @@ export type GlobalVersion = {
|
|||
|
||||
export type GlobalPluginData = {
|
||||
path: string;
|
||||
latestVersionName: DocsVersion;
|
||||
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({
|
||||
currentDocRoute,
|
||||
docsMetadata,
|
||||
versionMetadata,
|
||||
children,
|
||||
}): JSX.Element {
|
||||
const {siteConfig, isClient} = useDocusaurusContext();
|
||||
const {permalinkToSidebar, docsSidebars, version} = docsMetadata;
|
||||
const {permalinkToSidebar, docsSidebars, version} = versionMetadata;
|
||||
const sidebarName = permalinkToSidebar[currentDocRoute.path];
|
||||
const sidebar = docsSidebars[sidebarName];
|
||||
return (
|
||||
|
@ -52,7 +52,7 @@ function DocPageContent({
|
|||
function DocPage(props) {
|
||||
const {
|
||||
route: {routes: docRoutes},
|
||||
docsMetadata,
|
||||
versionMetadata,
|
||||
location,
|
||||
} = props;
|
||||
const currentDocRoute = docRoutes.find((docRoute) =>
|
||||
|
@ -64,7 +64,7 @@ function DocPage(props) {
|
|||
return (
|
||||
<DocPageContent
|
||||
currentDocRoute={currentDocRoute}
|
||||
docsMetadata={docsMetadata}>
|
||||
versionMetadata={versionMetadata}>
|
||||
{renderRoutes(docRoutes)}
|
||||
</DocPageContent>
|
||||
);
|
||||
|
|
|
@ -52,18 +52,21 @@ function DocVersionSuggestions(): JSX.Element {
|
|||
|
||||
return (
|
||||
<div className="alert alert--warning margin-bottom--md" role="alert">
|
||||
{activeVersionName === 'next' ? (
|
||||
<div>
|
||||
This is unreleased documentation for {siteTitle}{' '}
|
||||
<strong>{activeVersionName}</strong> version.
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
This is documentation for {siteTitle}{' '}
|
||||
<strong>v{activeVersionName}</strong>, which is no longer actively
|
||||
maintained.
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
// TODO need refactoring
|
||||
activeVersionName === 'current' ? (
|
||||
<div>
|
||||
This is unreleased documentation for {siteTitle}{' '}
|
||||
<strong>{activeVersionName}</strong> version.
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
This is documentation for {siteTitle}{' '}
|
||||
<strong>v{activeVersionName}</strong>, which is no longer actively
|
||||
maintained.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className="margin-top--md">
|
||||
For up-to-date documentation, see the{' '}
|
||||
<strong>
|
||||
|
|
|
@ -13,16 +13,13 @@ import {
|
|||
useActiveDocContext,
|
||||
} from '@theme/hooks/useDocs';
|
||||
|
||||
const versionLabel = (version, nextVersionLabel) =>
|
||||
version.name === 'next' ? nextVersionLabel : version.name;
|
||||
|
||||
const getVersionMainDoc = (version) =>
|
||||
version.docs.find((doc) => doc.id === version.mainDocId);
|
||||
|
||||
export default function DocsVersionDropdownNavbarItem({
|
||||
mobile,
|
||||
docsPluginId,
|
||||
nextVersionLabel,
|
||||
nextVersionLabel: _unused, // TODO legacy, remove asap
|
||||
...props
|
||||
}) {
|
||||
const activeDocContext = useActiveDocContext(docsPluginId);
|
||||
|
@ -37,7 +34,7 @@ export default function DocsVersionDropdownNavbarItem({
|
|||
getVersionMainDoc(version);
|
||||
return {
|
||||
isNavLink: true,
|
||||
label: versionLabel(version, nextVersionLabel),
|
||||
label: version.label,
|
||||
to: versionDoc.path,
|
||||
isActive: () => version === activeDocContext?.activeVersion,
|
||||
};
|
||||
|
@ -46,9 +43,7 @@ export default function DocsVersionDropdownNavbarItem({
|
|||
const dropdownVersion = activeDocContext.activeVersion ?? latestVersion;
|
||||
|
||||
// Mobile is handled a bit differently
|
||||
const dropdownLabel = mobile
|
||||
? 'Versions'
|
||||
: versionLabel(dropdownVersion, nextVersionLabel);
|
||||
const dropdownLabel = mobile ? 'Versions' : dropdownVersion.label;
|
||||
const dropdownTo = mobile
|
||||
? undefined
|
||||
: getVersionMainDoc(dropdownVersion).path;
|
||||
|
|
|
@ -12,20 +12,17 @@ import {useActiveVersion, useLatestVersion} from '@theme/hooks/useDocs';
|
|||
const getVersionMainDoc = (version) =>
|
||||
version.docs.find((doc) => doc.id === version.mainDocId);
|
||||
|
||||
const versionLabel = (version, nextVersionLabel) =>
|
||||
version.name === 'next' ? nextVersionLabel : version.name;
|
||||
|
||||
export default function DocsVersionNavbarItem({
|
||||
label: staticLabel,
|
||||
to: staticTo,
|
||||
docsPluginId,
|
||||
nextVersionLabel,
|
||||
nextVersionLabel: _unused, // TODO legacy, remove asap
|
||||
...props
|
||||
}) {
|
||||
const activeVersion = useActiveVersion(docsPluginId);
|
||||
const latestVersion = useLatestVersion(docsPluginId);
|
||||
const version = activeVersion ?? latestVersion;
|
||||
const label = staticLabel ?? versionLabel(version, nextVersionLabel);
|
||||
const label = staticLabel ?? version.label;
|
||||
const path = staticTo ?? getVersionMainDoc(version).path;
|
||||
return <DefaultNavbarItem {...props} label={label} to={path} />;
|
||||
}
|
||||
|
|
|
@ -50,14 +50,13 @@ const DocsVersionNavbarItemSchema = Joi.object({
|
|||
label: Joi.string(),
|
||||
to: Joi.string(),
|
||||
docsPluginId: Joi.string(),
|
||||
nextVersionLabel: Joi.string().default('Next'),
|
||||
});
|
||||
|
||||
const DocsVersionDropdownNavbarItemSchema = Joi.object({
|
||||
type: Joi.string().equal('docsVersionDropdown').required(),
|
||||
position: NavbarItemPosition,
|
||||
docsPluginId: Joi.string(),
|
||||
nextVersionLabel: Joi.string().default('Next'),
|
||||
nextVersionLabel: Joi.string().default('Next'), // TODO remove soon
|
||||
});
|
||||
|
||||
// Can this be made easier? :/
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
removePrefix,
|
||||
getFilePathForRoutePath,
|
||||
addLeadingSlash,
|
||||
getElementsAround,
|
||||
} from '../index';
|
||||
|
||||
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);
|
||||
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
|
||||
---
|
||||
|
||||
- [Advanced Guides – Themes](advanced-themes.md)
|
||||
- [Advanced Guides – Themes](using-themes.md)
|
||||
- [Lifecycle APIs](lifecycle-apis.md)
|
||||
|
||||
References
|
||||
|
|
|
@ -182,7 +182,7 @@ High-level overview about themes:
|
|||
Related pieces
|
||||
---
|
||||
|
||||
- [Advanced Guides – Themes](advanced-themes.md)
|
||||
- [Advanced Guides – Themes](using-themes.md)
|
||||
- [Lifecycle APIs](lifecycle-apis.md)
|
||||
|
||||
References
|
||||
|
|
|
@ -184,7 +184,7 @@ High-level overview about themes:
|
|||
Related pieces
|
||||
---
|
||||
|
||||
- [Advanced Guides – Themes](advanced-themes.md)
|
||||
- [Advanced Guides – Themes](using-themes.md)
|
||||
- [Lifecycle APIs](lifecycle-apis.md)
|
||||
|
||||
References
|
||||
|
|
|
@ -184,7 +184,7 @@ High-level overview about themes:
|
|||
Related pieces
|
||||
---
|
||||
|
||||
- [Advanced Guides – Themes](advanced-themes.md)
|
||||
- [Advanced Guides – Themes](using-themes.md)
|
||||
- [Lifecycle APIs](lifecycle-apis.md)
|
||||
|
||||
References
|
||||
|
|
|
@ -184,7 +184,7 @@ High-level overview about themes:
|
|||
Related pieces
|
||||
---
|
||||
|
||||
- [Advanced Guides – Themes](advanced-themes.md)
|
||||
- [Advanced Guides – Themes](using-themes.md)
|
||||
- [Lifecycle APIs](lifecycle-apis.md)
|
||||
|
||||
References
|
||||
|
|
|
@ -189,7 +189,7 @@ High-level overview about themes:
|
|||
Related pieces
|
||||
---
|
||||
|
||||
- [Advanced Guides – Themes](advanced-themes.md)
|
||||
- [Advanced Guides – Themes](using-themes.md)
|
||||
- [Lifecycle APIs](lifecycle-apis.md)
|
||||
|
||||
References
|
||||
|
|
|
@ -189,7 +189,7 @@ High-level overview about themes:
|
|||
Related pieces
|
||||
---
|
||||
|
||||
- [Advanced Guides – Themes](advanced-themes.md)
|
||||
- [Advanced Guides – Themes](using-themes.md)
|
||||
- [Lifecycle APIs](lifecycle-apis.md)
|
||||
|
||||
References
|
||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -4074,6 +4074,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109"
|
||||
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":
|
||||
version "1.19.1"
|
||||
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"
|
||||
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:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||
|
|
Loading…
Add table
Reference in a new issue