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:
Sébastien Lorber 2020-08-17 17:50:22 +02:00 committed by GitHub
parent d17df954b5
commit a4c8a7f55b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 3219 additions and 2724 deletions

View file

@ -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,

View file

@ -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",

View file

@ -0,0 +1,5 @@
{
"version-1.0.1/docs": {
"Test": ["version-withSlugs/rootAbsoluteSlug"]
}
}

View file

@ -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(

View file

@ -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',
);
});
});

View file

@ -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();
});
});

View file

@ -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();
});

View file

@ -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.',

View file

@ -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',
);
});
});

View file

@ -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(

View file

@ -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"`,
);
});
});

View file

@ -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,
});
});
});

View file

@ -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"`,
);
});
});

View file

@ -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`,

View file

@ -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,
};

View file

@ -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,

View file

@ -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';

View 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;
}

View file

@ -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,
};
}

View 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),
};
}

View file

@ -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';

View file

@ -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) {

View file

@ -0,0 +1,6 @@
### Not Existing Docs
- [docNotExist1](docNotExist1.md)
- [docNotExist2](./docNotExist2.mdx)
- [docNotExist3](../docNotExist3.mdx)
- [docNotExist4](./subdir/docNotExist4.md)

View file

@ -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');

View file

@ -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))
);
};

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View 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,
};
}

View file

@ -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,
};
}

View file

@ -14,6 +14,7 @@ try {
versions = [];
}
// TODO deprecate in favor of useDocs.ts instead
function useVersioning(): {
versioningEnabled: boolean;
versions: string[];

View file

@ -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;
};

View 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;
}

View file

@ -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>
);

View file

@ -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>

View file

@ -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;

View file

@ -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} />;
}

View file

@ -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? :/

View file

@ -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"`,
);
});
});

View file

@ -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};
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"