feat(v2): docs version configuration: lastVersion, version.{path,label} (#3357)

* add new docs versioning options

* Add some tests for new versioning options

* Add some docs for version configurations

* try to fix broken link detection after /docs/ root paths have been removed on deploy previews

* improve dev/deploypreview versioning configurations

* disable custom current version path, as it produces broken links

* readVersionDocs should not be order sensitive

* fix versions page according to versioning config

* fix versions page according to versioning config
This commit is contained in:
Sébastien Lorber 2020-08-28 18:37:49 +02:00 committed by GitHub
parent 4bfc3bbbe7
commit ae877f2990
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 387 additions and 79 deletions

View file

@ -290,6 +290,7 @@ Object {
}",
"version-current-metadata-prop-751.json": "{
\\"version\\": \\"current\\",
\\"label\\": \\"Next\\",
\\"docsSidebars\\": {
\\"docs\\": [
{
@ -613,6 +614,7 @@ Object {
}",
"version-1-0-0-metadata-prop-608.json": "{
\\"version\\": \\"1.0.0\\",
\\"label\\": \\"1.0.0\\",
\\"docsSidebars\\": {
\\"version-1.0.0/community\\": [
{
@ -628,6 +630,7 @@ Object {
}",
"version-current-metadata-prop-751.json": "{
\\"version\\": \\"current\\",
\\"label\\": \\"Next\\",
\\"docsSidebars\\": {
\\"community\\": [
{
@ -1069,6 +1072,7 @@ Object {
}",
"version-1-0-0-metadata-prop-608.json": "{
\\"version\\": \\"1.0.0\\",
\\"label\\": \\"1.0.0\\",
\\"docsSidebars\\": {
\\"version-1.0.0/docs\\": [
{
@ -1110,6 +1114,7 @@ Object {
}",
"version-1-0-1-metadata-prop-e87.json": "{
\\"version\\": \\"1.0.1\\",
\\"label\\": \\"1.0.1\\",
\\"docsSidebars\\": {
\\"version-1.0.1/docs\\": [
{
@ -1145,6 +1150,7 @@ Object {
}",
"version-current-metadata-prop-751.json": "{
\\"version\\": \\"current\\",
\\"label\\": \\"Next\\",
\\"docsSidebars\\": {
\\"docs\\": [
{
@ -1180,6 +1186,7 @@ Object {
}",
"version-with-slugs-metadata-prop-2bf.json": "{
\\"version\\": \\"withSlugs\\",
\\"label\\": \\"withSlugs\\",
\\"docsSidebars\\": {
\\"version-1.0.1/docs\\": [
{

View file

@ -135,21 +135,23 @@ describe('simple site', () => {
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',
]);
expect(docs.map((doc) => doc.source).sort()).toEqual(
[
'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',
].sort(),
);
});
test('normal docs', async () => {

View file

@ -36,6 +36,16 @@ describe('normalizeDocsPluginOptions', () => {
excludeNextVersionDocs: true,
includeCurrentVersion: false,
disableVersioning: true,
versions: {
current: {
path: 'next',
label: 'next',
},
version1: {
path: 'hello',
label: 'world',
},
},
};
const {value, error} = await OptionsSchema.validate(userOptions);
expect(value).toEqual(userOptions);
@ -117,4 +127,32 @@ describe('normalizeDocsPluginOptions', () => {
`"\\"remarkPlugins\\" must be an array"`,
);
});
test('should reject bad lastVersion', () => {
expect(() => {
normalizePluginOptions(OptionsSchema, {
lastVersion: false,
});
}).toThrowErrorMatchingInlineSnapshot(
`"\\"lastVersion\\" must be a string"`,
);
});
test('should reject bad versions', () => {
expect(() => {
normalizePluginOptions(OptionsSchema, {
versions: {
current: {
hey: 3,
},
version1: {
path: 'hello',
label: 'world',
},
},
});
}).toThrowErrorMatchingInlineSnapshot(
`"\\"versions.current.hey\\" is not allowed"`,
);
});
});

View file

@ -94,7 +94,63 @@ describe('simple site', () => {
]);
});
test('readVersionsMetadata simple site with base url', () => {
test('readVersionsMetadata simple site with current version config', () => {
const versionsMetadata = readVersionsMetadata({
options: {
...defaultOptions,
versions: {
current: {
label: 'current-label',
path: 'current-path',
},
},
},
context: {
...defaultContext,
baseUrl: '/myBaseUrl',
},
});
expect(versionsMetadata).toEqual([
{
...vCurrent,
versionPath: '/myBaseUrl/docs/current-path',
versionLabel: 'current-label',
routePriority: undefined,
},
]);
});
test('readVersionsMetadata simple site with unknown lastVersion should throw', () => {
expect(() =>
readVersionsMetadata({
options: {...defaultOptions, lastVersion: 'unknownVersionName'},
context: defaultContext,
}),
).toThrowErrorMatchingInlineSnapshot(
`"Docs option lastVersion=unknownVersionName is invalid. Available version names are: current"`,
);
});
test('readVersionsMetadata simple site with unknown version configurations should throw', () => {
expect(() =>
readVersionsMetadata({
options: {
...defaultOptions,
versions: {
current: {label: 'current'},
unknownVersionName1: {label: 'unknownVersionName1'},
unknownVersionName2: {label: 'unknownVersionName2'},
},
},
context: defaultContext,
}),
).toThrowErrorMatchingInlineSnapshot(
`"Docs versions option provided configuration for unknown versions: unknownVersionName1,unknownVersionName2. Available version names are: current"`,
);
});
test('readVersionsMetadata simple site with disableVersioning while single version should throw', () => {
expect(() =>
readVersionsMetadata({
options: {...defaultOptions, disableVersioning: true},
@ -105,7 +161,7 @@ describe('simple site', () => {
);
});
test('readVersionsMetadata simple site with base url', () => {
test('readVersionsMetadata simple site without including current version should throw', () => {
expect(() =>
readVersionsMetadata({
options: {...defaultOptions, includeCurrentVersion: false},
@ -205,6 +261,42 @@ describe('versioned site, pluginId=default', () => {
]);
});
test('readVersionsMetadata versioned site with version options', () => {
const versionsMetadata = readVersionsMetadata({
options: {
...defaultOptions,
lastVersion: '1.0.0',
versions: {
current: {
path: 'current-path',
},
'1.0.0': {
label: '1.0.0-label',
},
},
},
context: defaultContext,
});
expect(versionsMetadata).toEqual([
{...vCurrent, versionPath: '/docs/current-path'},
{
...v101,
isLast: false,
routePriority: undefined,
versionPath: '/docs/1.0.1',
},
{
...v100,
isLast: true,
routePriority: -1,
versionLabel: '1.0.0-label',
versionPath: '/docs',
},
vwithSlugs,
]);
});
test('readVersionsMetadata versioned site with disableVersioning', () => {
const versionsMetadata = readVersionsMetadata({
options: {...defaultOptions, disableVersioning: true},

View file

@ -33,8 +33,19 @@ export const DEFAULT_OPTIONS: Omit<PluginOptions, 'id'> = {
excludeNextVersionDocs: false,
includeCurrentVersion: true,
disableVersioning: false,
lastVersion: undefined,
versions: {},
};
const VersionOptionsSchema = Joi.object({
path: Joi.string().allow('').optional(),
label: Joi.string().optional(),
});
const VersionsOptionsSchema = Joi.object()
.pattern(Joi.string().required(), VersionOptionsSchema)
.default(DEFAULT_OPTIONS.versions);
export const OptionsSchema = Joi.object({
path: Joi.string().default(DEFAULT_OPTIONS.path),
editUrl: URISchema,
@ -58,6 +69,8 @@ export const OptionsSchema = Joi.object({
DEFAULT_OPTIONS.includeCurrentVersion,
),
disableVersioning: Joi.bool().default(DEFAULT_OPTIONS.disableVersioning),
lastVersion: Joi.string().optional(),
versions: VersionsOptionsSchema,
});
// TODO bad validation function types

View file

@ -8,14 +8,13 @@
/* eslint-disable camelcase */
declare module '@docusaurus/plugin-content-docs-types' {
export type VersionName = string;
export type PermalinkToSidebar = {
[permalink: string]: string;
};
export type PropVersionMetadata = {
version: VersionName;
version: string;
label: string;
docsSidebars: PropSidebars;
permalinkToSidebar: PermalinkToSidebar;
};

View file

@ -66,6 +66,7 @@ export function toVersionMetadataProp(
): PropVersionMetadata {
return {
version: loadedVersion.versionName,
label: loadedVersion.versionLabel,
docsSidebars: toSidebarsProp(loadedVersion),
permalinkToSidebar: loadedVersion.permalinkToSidebar,
};

View file

@ -39,8 +39,19 @@ export type PathOptions = {
sidebarPath: string;
};
export type VersionOptions = {
path?: string;
label?: string;
};
export type VersionsOptions = {
lastVersion?: string;
versions: Record<string, VersionOptions>;
};
export type PluginOptions = MetadataOptions &
PathOptions & {
PathOptions &
VersionsOptions & {
id: string;
include: string[];
docLayoutComponent: string;

View file

@ -7,7 +7,12 @@
import path from 'path';
import fs from 'fs-extra';
import {PluginOptions, VersionMetadata} from './types';
import {
PluginOptions,
VersionMetadata,
VersionOptions,
VersionsOptions,
} from './types';
import {
VERSIONS_JSON_FILE,
VERSIONED_DOCS_DIR,
@ -18,6 +23,7 @@ import {
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
import {LoadContext} from '@docusaurus/types';
import {normalizeUrl} from '@docusaurus/utils';
import {difference} from 'lodash';
// retro-compatibility: no prefix for the default plugin id
function addPluginIdPrefix(fileOrDir: string, pluginId: string): string {
@ -161,7 +167,10 @@ function createVersionMetadata({
versionName: string;
isLast: boolean;
context: Pick<LoadContext, 'siteDir' | 'baseUrl'>;
options: Pick<PluginOptions, 'id' | 'path' | 'sidebarPath' | 'routeBasePath'>;
options: Pick<
PluginOptions,
'id' | 'path' | 'sidebarPath' | 'routeBasePath' | 'versions'
>;
}): VersionMetadata {
const {sidebarFilePath, docsDirPath} = getVersionMetadataPaths({
versionName,
@ -169,16 +178,20 @@ function createVersionMetadata({
options,
});
// TODO hardcoded for retro-compatibility
// TODO Need to make this configurable
const versionLabel =
// retro-compatible values
const defaultVersionLabel =
versionName === CURRENT_VERSION_NAME ? 'Next' : versionName;
const versionPathPart = isLast
const defaultVersionPathPart = isLast
? ''
: versionName === CURRENT_VERSION_NAME
? 'next'
: versionName;
const versionOptions: VersionOptions = options.versions[versionName] ?? {};
const versionLabel = versionOptions.label ?? defaultVersionLabel;
const versionPathPart = versionOptions.path ?? defaultVersionPathPart;
const versionPath = normalizeUrl([
context.baseUrl,
options.routeBasePath,
@ -219,7 +232,7 @@ function checkVersionMetadataPaths({
// 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[]) {
function getDefaultLastVersionName(versionNames: string[]) {
if (versionNames.length === 1) {
return versionNames[0];
} else {
@ -229,6 +242,34 @@ function getLastVersionName(versionNames: string[]) {
}
}
function checkVersionsOptions(
availableVersionNames: string[],
options: VersionsOptions,
) {
const availableVersionNamesMsg = `Available version names are: ${availableVersionNames.join(
', ',
)}`;
if (
options.lastVersion &&
!availableVersionNames.includes(options.lastVersion)
) {
throw new Error(
`Docs option lastVersion=${options.lastVersion} is invalid. ${availableVersionNamesMsg}`,
);
}
const unknownVersionNames = difference(
Object.keys(options.versions),
availableVersionNames,
);
if (unknownVersionNames.length > 0) {
throw new Error(
`Docs versions option provided configuration for unknown versions: ${unknownVersionNames.join(
',',
)}. ${availableVersionNamesMsg}`,
);
}
}
export function readVersionsMetadata({
context,
options,
@ -242,10 +283,17 @@ export function readVersionsMetadata({
| 'routeBasePath'
| 'includeCurrentVersion'
| 'disableVersioning'
| 'lastVersion'
| 'versions'
>;
}): VersionMetadata[] {
const versionNames = readVersionNames(context.siteDir, options);
const lastVersionName = getLastVersionName(versionNames);
checkVersionsOptions(versionNames, options);
const lastVersionName =
options.lastVersion ?? getDefaultLastVersionName(versionNames);
const versionsMetadata = versionNames.map((versionName) =>
createVersionMetadata({
versionName,