mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 15:47:23 +02:00
refactor(content-docs): split version handling into several files (#7140)
* refactor(content-docs): split version handling into several files * fix test * increase timeout
This commit is contained in:
parent
7d44961d8b
commit
96fbcb3f51
12 changed files with 618 additions and 698 deletions
Binary file not shown.
|
@ -33,6 +33,8 @@ import type {Optional} from 'utility-types';
|
||||||
import {createSlugger, posixPath, DEFAULT_PLUGIN_ID} from '@docusaurus/utils';
|
import {createSlugger, posixPath, DEFAULT_PLUGIN_ID} from '@docusaurus/utils';
|
||||||
import {createSidebarsUtils} from '../sidebars/utils';
|
import {createSidebarsUtils} from '../sidebars/utils';
|
||||||
|
|
||||||
|
jest.setTimeout(15000);
|
||||||
|
|
||||||
const fixtureDir = path.join(__dirname, '__fixtures__');
|
const fixtureDir = path.join(__dirname, '__fixtures__');
|
||||||
|
|
||||||
const createFakeDocFile = ({
|
const createFakeDocFile = ({
|
||||||
|
|
|
@ -7,14 +7,15 @@
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getVersionsFilePath,
|
getVersionsFilePath,
|
||||||
getVersionedDocsDirPath,
|
getVersionDocsDirPath,
|
||||||
getVersionedSidebarsDirPath,
|
getVersionSidebarsPath,
|
||||||
getDocsDirPathLocalized,
|
getDocsDirPathLocalized,
|
||||||
} from './versions';
|
} from './versions/files';
|
||||||
|
import {validateVersionName} from './versions/validation';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type {PluginOptions} from '@docusaurus/plugin-content-docs';
|
import type {PluginOptions} from '@docusaurus/plugin-content-docs';
|
||||||
import {loadSidebarsFileUnsafe, resolveSidebarPathOption} from './sidebars';
|
import {loadSidebarsFileUnsafe} from './sidebars';
|
||||||
import {CURRENT_VERSION_NAME} from './constants';
|
import {CURRENT_VERSION_NAME} from './constants';
|
||||||
import {DEFAULT_PLUGIN_ID} from '@docusaurus/utils';
|
import {DEFAULT_PLUGIN_ID} from '@docusaurus/utils';
|
||||||
import logger from '@docusaurus/logger';
|
import logger from '@docusaurus/logger';
|
||||||
|
@ -42,13 +43,8 @@ async function createVersionedSidebarFile({
|
||||||
const shouldCreateVersionedSidebarFile = Object.keys(sidebars).length > 0;
|
const shouldCreateVersionedSidebarFile = Object.keys(sidebars).length > 0;
|
||||||
|
|
||||||
if (shouldCreateVersionedSidebarFile) {
|
if (shouldCreateVersionedSidebarFile) {
|
||||||
const versionedSidebarsDir = getVersionedSidebarsDirPath(siteDir, pluginId);
|
|
||||||
const newSidebarFile = path.join(
|
|
||||||
versionedSidebarsDir,
|
|
||||||
`version-${version}-sidebars.json`,
|
|
||||||
);
|
|
||||||
await fs.outputFile(
|
await fs.outputFile(
|
||||||
newSidebarFile,
|
getVersionSidebarsPath(siteDir, pluginId, version),
|
||||||
`${JSON.stringify(sidebars, null, 2)}\n`,
|
`${JSON.stringify(sidebars, null, 2)}\n`,
|
||||||
'utf8',
|
'utf8',
|
||||||
);
|
);
|
||||||
|
@ -57,7 +53,7 @@ async function createVersionedSidebarFile({
|
||||||
|
|
||||||
// Tests depend on non-default export for mocking.
|
// Tests depend on non-default export for mocking.
|
||||||
export async function cliDocsVersionCommand(
|
export async function cliDocsVersionCommand(
|
||||||
version: string | null | undefined,
|
version: string,
|
||||||
{id: pluginId, path: docsPath, sidebarPath}: PluginOptions,
|
{id: pluginId, path: docsPath, sidebarPath}: PluginOptions,
|
||||||
{siteDir, i18n}: LoadContext,
|
{siteDir, i18n}: LoadContext,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
@ -66,44 +62,18 @@ export async function cliDocsVersionCommand(
|
||||||
const pluginIdLogPrefix =
|
const pluginIdLogPrefix =
|
||||||
pluginId === DEFAULT_PLUGIN_ID ? '[docs]' : `[${pluginId}]`;
|
pluginId === DEFAULT_PLUGIN_ID ? '[docs]' : `[${pluginId}]`;
|
||||||
|
|
||||||
if (!version) {
|
try {
|
||||||
throw new Error(
|
validateVersionName(version);
|
||||||
`${pluginIdLogPrefix}: no version tag specified! Pass the version you wish to create as an argument, for example: 1.0.0.`,
|
} catch (e) {
|
||||||
);
|
logger.info`${pluginIdLogPrefix}: Invalid version name provided. Try something like: 1.0.0`;
|
||||||
}
|
throw e;
|
||||||
|
|
||||||
if (version.includes('/') || version.includes('\\')) {
|
|
||||||
throw new Error(
|
|
||||||
`${pluginIdLogPrefix}: invalid version tag specified! Do not include slash (/) or backslash (\\). Try something like: 1.0.0.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (version.length > 32) {
|
|
||||||
throw new Error(
|
|
||||||
`${pluginIdLogPrefix}: invalid version tag specified! Length cannot exceed 32 characters. Try something like: 1.0.0.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since we are going to create `version-${version}` folder, we need to make
|
|
||||||
// sure it's a valid pathname.
|
|
||||||
// eslint-disable-next-line no-control-regex
|
|
||||||
if (/[<>:"|?*\x00-\x1F]/.test(version)) {
|
|
||||||
throw new Error(
|
|
||||||
`${pluginIdLogPrefix}: invalid version tag specified! Please ensure its a valid pathname too. Try something like: 1.0.0.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/^\.\.?$/.test(version)) {
|
|
||||||
throw new Error(
|
|
||||||
`${pluginIdLogPrefix}: invalid version tag specified! Do not name your version "." or "..". Try something like: 1.0.0.`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load existing versions.
|
// Load existing versions.
|
||||||
let versions = [];
|
let versions = [];
|
||||||
const versionsJSONFile = getVersionsFilePath(siteDir, pluginId);
|
const versionsJSONFile = getVersionsFilePath(siteDir, pluginId);
|
||||||
if (await fs.pathExists(versionsJSONFile)) {
|
if (await fs.pathExists(versionsJSONFile)) {
|
||||||
versions = JSON.parse(await fs.readFile(versionsJSONFile, 'utf8'));
|
versions = await fs.readJSON(versionsJSONFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if version already exists.
|
// Check if version already exists.
|
||||||
|
@ -146,10 +116,7 @@ export async function cliDocsVersionCommand(
|
||||||
|
|
||||||
const newVersionDir =
|
const newVersionDir =
|
||||||
locale === i18n.defaultLocale
|
locale === i18n.defaultLocale
|
||||||
? path.join(
|
? getVersionDocsDirPath(siteDir, pluginId, version)
|
||||||
getVersionedDocsDirPath(siteDir, pluginId),
|
|
||||||
`version-${version}`,
|
|
||||||
)
|
|
||||||
: getDocsDirPathLocalized({
|
: getDocsDirPathLocalized({
|
||||||
siteDir,
|
siteDir,
|
||||||
locale,
|
locale,
|
||||||
|
@ -164,7 +131,7 @@ export async function cliDocsVersionCommand(
|
||||||
siteDir,
|
siteDir,
|
||||||
pluginId,
|
pluginId,
|
||||||
version,
|
version,
|
||||||
sidebarPath: resolveSidebarPathOption(siteDir, sidebarPath),
|
sidebarPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update versions.json file.
|
// Update versions.json file.
|
||||||
|
|
|
@ -5,9 +5,11 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// The name of the version at the root of your site (website/docs)
|
/** The name of the version that's actively worked on (e.g. `website/docs`) */
|
||||||
export const CURRENT_VERSION_NAME = 'current';
|
export const CURRENT_VERSION_NAME = 'current';
|
||||||
|
/** All doc versions are stored here by version names */
|
||||||
export const VERSIONED_DOCS_DIR = 'versioned_docs';
|
export const VERSIONED_DOCS_DIR = 'versioned_docs';
|
||||||
|
/** All doc versioned sidebars are stored here by version names */
|
||||||
export const VERSIONED_SIDEBARS_DIR = 'versioned_sidebars';
|
export const VERSIONED_SIDEBARS_DIR = 'versioned_sidebars';
|
||||||
|
/** The version names. Should 1-1 map to the content of versioned docs dir. */
|
||||||
export const VERSIONS_JSON_FILE = 'versions.json';
|
export const VERSIONS_JSON_FILE = 'versions.json';
|
||||||
|
|
|
@ -20,7 +20,7 @@ import {
|
||||||
DEFAULT_PLUGIN_ID,
|
DEFAULT_PLUGIN_ID,
|
||||||
} from '@docusaurus/utils';
|
} from '@docusaurus/utils';
|
||||||
import type {LoadContext, Plugin} from '@docusaurus/types';
|
import type {LoadContext, Plugin} from '@docusaurus/types';
|
||||||
import {loadSidebars} from './sidebars';
|
import {loadSidebars, resolveSidebarPathOption} from './sidebars';
|
||||||
import {CategoryMetadataFilenamePattern} from './sidebars/generator';
|
import {CategoryMetadataFilenamePattern} from './sidebars/generator';
|
||||||
import {
|
import {
|
||||||
readVersionDocs,
|
readVersionDocs,
|
||||||
|
@ -64,6 +64,8 @@ export default async function pluginContentDocs(
|
||||||
options: PluginOptions,
|
options: PluginOptions,
|
||||||
): Promise<Plugin<LoadedContent>> {
|
): Promise<Plugin<LoadedContent>> {
|
||||||
const {siteDir, generatedFilesDir, baseUrl, siteConfig} = context;
|
const {siteDir, generatedFilesDir, baseUrl, siteConfig} = context;
|
||||||
|
// Mutate options to resolve sidebar path according to siteDir
|
||||||
|
options.sidebarPath = resolveSidebarPathOption(siteDir, options.sidebarPath);
|
||||||
|
|
||||||
const versionsMetadata = await readVersionsMetadata({context, options});
|
const versionsMetadata = await readVersionsMetadata({context, options});
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,5 @@ export {
|
||||||
getDefaultVersionBanner,
|
getDefaultVersionBanner,
|
||||||
getVersionBadge,
|
getVersionBadge,
|
||||||
getVersionBanner,
|
getVersionBanner,
|
||||||
getVersionsFilePath,
|
|
||||||
readVersionsFile,
|
|
||||||
readVersionNames,
|
|
||||||
} from './versions';
|
} from './versions';
|
||||||
|
export {readVersionNames} from './versions/files';
|
||||||
|
|
|
@ -32,7 +32,6 @@ export const DefaultSidebars: SidebarsConfig = {
|
||||||
export const DisabledSidebars: SidebarsConfig = {};
|
export const DisabledSidebars: SidebarsConfig = {};
|
||||||
|
|
||||||
// If a path is provided, make it absolute
|
// If a path is provided, make it absolute
|
||||||
// use this before loadSidebars()
|
|
||||||
export function resolveSidebarPathOption(
|
export function resolveSidebarPathOption(
|
||||||
siteDir: string,
|
siteDir: string,
|
||||||
sidebarPathOption: PluginOptions['sidebarPath'],
|
sidebarPathOption: PluginOptions['sidebarPath'],
|
||||||
|
@ -93,7 +92,6 @@ export async function loadSidebarsFileUnsafe(
|
||||||
return importFresh(sidebarFilePath);
|
return importFresh(sidebarFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: sidebarFilePath must be absolute, use resolveSidebarPathOption
|
|
||||||
export async function loadSidebars(
|
export async function loadSidebars(
|
||||||
sidebarFilePath: string | false | undefined,
|
sidebarFilePath: string | false | undefined,
|
||||||
options: SidebarProcessorParams,
|
options: SidebarProcessorParams,
|
||||||
|
|
|
@ -1,593 +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 {
|
|
||||||
VERSIONS_JSON_FILE,
|
|
||||||
VERSIONED_DOCS_DIR,
|
|
||||||
VERSIONED_SIDEBARS_DIR,
|
|
||||||
CURRENT_VERSION_NAME,
|
|
||||||
} from './constants';
|
|
||||||
import type {
|
|
||||||
PluginOptions,
|
|
||||||
VersionBanner,
|
|
||||||
VersionsOptions,
|
|
||||||
VersionMetadata,
|
|
||||||
} from '@docusaurus/plugin-content-docs';
|
|
||||||
|
|
||||||
import type {LoadContext} from '@docusaurus/types';
|
|
||||||
import {
|
|
||||||
getPluginI18nPath,
|
|
||||||
normalizeUrl,
|
|
||||||
posixPath,
|
|
||||||
DEFAULT_PLUGIN_ID,
|
|
||||||
} from '@docusaurus/utils';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import {resolveSidebarPathOption} from './sidebars';
|
|
||||||
|
|
||||||
// retro-compatibility: no prefix for the default plugin id
|
|
||||||
function addPluginIdPrefix(fileOrDir: string, pluginId: string): string {
|
|
||||||
return pluginId === DEFAULT_PLUGIN_ID
|
|
||||||
? fileOrDir
|
|
||||||
: `${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 (!Array.isArray(versionArray)) {
|
|
||||||
throw new Error(
|
|
||||||
`The versions file should contain an array of version names! Found content: ${JSON.stringify(
|
|
||||||
versionArray,
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
versionArray.forEach(ensureValidVersionString);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function readVersionsFile(
|
|
||||||
siteDir: string,
|
|
||||||
pluginId: string,
|
|
||||||
): Promise<string[] | null> {
|
|
||||||
const versionsFilePath = getVersionsFilePath(siteDir, pluginId);
|
|
||||||
if (await fs.pathExists(versionsFilePath)) {
|
|
||||||
const content = JSON.parse(await fs.readFile(versionsFilePath, 'utf8'));
|
|
||||||
ensureValidVersionArray(content);
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function readVersionNames(
|
|
||||||
siteDir: string,
|
|
||||||
options: Pick<
|
|
||||||
PluginOptions,
|
|
||||||
'id' | 'disableVersioning' | 'includeCurrentVersion'
|
|
||||||
>,
|
|
||||||
): Promise<string[]> {
|
|
||||||
const versionFileContent = await 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; or
|
|
||||||
// - it's already 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDocsDirPathLocalized({
|
|
||||||
siteDir,
|
|
||||||
locale,
|
|
||||||
pluginId,
|
|
||||||
versionName,
|
|
||||||
}: {
|
|
||||||
siteDir: string;
|
|
||||||
locale: string;
|
|
||||||
pluginId: string;
|
|
||||||
versionName: string;
|
|
||||||
}): string {
|
|
||||||
return getPluginI18nPath({
|
|
||||||
siteDir,
|
|
||||||
locale,
|
|
||||||
pluginName: 'docusaurus-plugin-content-docs',
|
|
||||||
pluginId,
|
|
||||||
subPaths: [
|
|
||||||
versionName === CURRENT_VERSION_NAME
|
|
||||||
? CURRENT_VERSION_NAME
|
|
||||||
: `version-${versionName}`,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVersionMetadataPaths({
|
|
||||||
versionName,
|
|
||||||
context,
|
|
||||||
options,
|
|
||||||
}: {
|
|
||||||
versionName: string;
|
|
||||||
context: Pick<LoadContext, 'siteDir' | 'i18n'>;
|
|
||||||
options: Pick<PluginOptions, 'id' | 'path' | 'sidebarPath'>;
|
|
||||||
}): Pick<
|
|
||||||
VersionMetadata,
|
|
||||||
'contentPath' | 'contentPathLocalized' | 'sidebarFilePath'
|
|
||||||
> {
|
|
||||||
const isCurrentVersion = versionName === CURRENT_VERSION_NAME;
|
|
||||||
|
|
||||||
const contentPathLocalized = getDocsDirPathLocalized({
|
|
||||||
siteDir: context.siteDir,
|
|
||||||
locale: context.i18n.currentLocale,
|
|
||||||
pluginId: options.id,
|
|
||||||
versionName,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isCurrentVersion) {
|
|
||||||
return {
|
|
||||||
contentPath: path.resolve(context.siteDir, options.path),
|
|
||||||
contentPathLocalized,
|
|
||||||
sidebarFilePath: resolveSidebarPathOption(
|
|
||||||
context.siteDir,
|
|
||||||
options.sidebarPath,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
contentPath: path.join(
|
|
||||||
getVersionedDocsDirPath(context.siteDir, options.id),
|
|
||||||
`version-${versionName}`,
|
|
||||||
),
|
|
||||||
contentPathLocalized,
|
|
||||||
sidebarFilePath: path.join(
|
|
||||||
getVersionedSidebarsDirPath(context.siteDir, options.id),
|
|
||||||
`version-${versionName}-sidebars.json`,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVersionEditUrls({
|
|
||||||
contentPath,
|
|
||||||
contentPathLocalized,
|
|
||||||
context: {siteDir, i18n},
|
|
||||||
options: {
|
|
||||||
id,
|
|
||||||
path: currentVersionPath,
|
|
||||||
editUrl: editUrlOption,
|
|
||||||
editCurrentVersion,
|
|
||||||
},
|
|
||||||
}: {
|
|
||||||
contentPath: string;
|
|
||||||
contentPathLocalized: string;
|
|
||||||
context: Pick<LoadContext, 'siteDir' | 'i18n'>;
|
|
||||||
options: Pick<
|
|
||||||
PluginOptions,
|
|
||||||
'id' | 'path' | 'editUrl' | 'editCurrentVersion'
|
|
||||||
>;
|
|
||||||
}): Pick<VersionMetadata, 'editUrl' | 'editUrlLocalized'> {
|
|
||||||
// If the user is using the functional form of editUrl,
|
|
||||||
// she has total freedom and we can't compute a "version edit url"
|
|
||||||
if (!editUrlOption || typeof editUrlOption === 'function') {
|
|
||||||
return {editUrl: undefined, editUrlLocalized: undefined};
|
|
||||||
}
|
|
||||||
|
|
||||||
const editDirPath = editCurrentVersion ? currentVersionPath : contentPath;
|
|
||||||
const editDirPathLocalized = editCurrentVersion
|
|
||||||
? getDocsDirPathLocalized({
|
|
||||||
siteDir,
|
|
||||||
locale: i18n.currentLocale,
|
|
||||||
versionName: CURRENT_VERSION_NAME,
|
|
||||||
pluginId: id,
|
|
||||||
})
|
|
||||||
: contentPathLocalized;
|
|
||||||
|
|
||||||
const versionPathSegment = posixPath(
|
|
||||||
path.relative(siteDir, path.resolve(siteDir, editDirPath)),
|
|
||||||
);
|
|
||||||
const versionPathSegmentLocalized = posixPath(
|
|
||||||
path.relative(siteDir, path.resolve(siteDir, editDirPathLocalized)),
|
|
||||||
);
|
|
||||||
|
|
||||||
const editUrl = normalizeUrl([editUrlOption, versionPathSegment]);
|
|
||||||
|
|
||||||
const editUrlLocalized = normalizeUrl([
|
|
||||||
editUrlOption,
|
|
||||||
versionPathSegmentLocalized,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
editUrl,
|
|
||||||
editUrlLocalized,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDefaultVersionBanner({
|
|
||||||
versionName,
|
|
||||||
versionNames,
|
|
||||||
lastVersionName,
|
|
||||||
}: {
|
|
||||||
versionName: string;
|
|
||||||
versionNames: string[];
|
|
||||||
lastVersionName: string;
|
|
||||||
}): VersionBanner | null {
|
|
||||||
// Current version: good, no banner
|
|
||||||
if (versionName === lastVersionName) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// Upcoming versions: unreleased banner
|
|
||||||
if (
|
|
||||||
versionNames.indexOf(versionName) < versionNames.indexOf(lastVersionName)
|
|
||||||
) {
|
|
||||||
return 'unreleased';
|
|
||||||
}
|
|
||||||
// Older versions: display unmaintained banner
|
|
||||||
return 'unmaintained';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getVersionBanner({
|
|
||||||
versionName,
|
|
||||||
versionNames,
|
|
||||||
lastVersionName,
|
|
||||||
options,
|
|
||||||
}: {
|
|
||||||
versionName: string;
|
|
||||||
versionNames: string[];
|
|
||||||
lastVersionName: string;
|
|
||||||
options: Pick<PluginOptions, 'versions'>;
|
|
||||||
}): VersionBanner | null {
|
|
||||||
const versionBannerOption = options.versions[versionName]?.banner;
|
|
||||||
if (versionBannerOption) {
|
|
||||||
return versionBannerOption === 'none' ? null : versionBannerOption;
|
|
||||||
}
|
|
||||||
return getDefaultVersionBanner({
|
|
||||||
versionName,
|
|
||||||
versionNames,
|
|
||||||
lastVersionName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getVersionBadge({
|
|
||||||
versionName,
|
|
||||||
versionNames,
|
|
||||||
options,
|
|
||||||
}: {
|
|
||||||
versionName: string;
|
|
||||||
versionNames: string[];
|
|
||||||
options: Pick<PluginOptions, 'versions'>;
|
|
||||||
}): boolean {
|
|
||||||
const versionBadgeOption = options.versions[versionName]?.badge;
|
|
||||||
// If site is not versioned or only one version is included
|
|
||||||
// we don't show the version badge by default
|
|
||||||
// See https://github.com/facebook/docusaurus/issues/3362
|
|
||||||
const versionBadgeDefault = versionNames.length !== 1;
|
|
||||||
return versionBadgeOption ?? versionBadgeDefault;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVersionClassName({
|
|
||||||
versionName,
|
|
||||||
options,
|
|
||||||
}: {
|
|
||||||
versionName: string;
|
|
||||||
options: Pick<PluginOptions, 'versions'>;
|
|
||||||
}): string {
|
|
||||||
const versionClassNameOption = options.versions[versionName]?.className;
|
|
||||||
const versionClassNameDefault = `docs-version-${versionName}`;
|
|
||||||
return versionClassNameOption ?? versionClassNameDefault;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createVersionMetadata({
|
|
||||||
versionName,
|
|
||||||
versionNames,
|
|
||||||
lastVersionName,
|
|
||||||
context,
|
|
||||||
options,
|
|
||||||
}: {
|
|
||||||
versionName: string;
|
|
||||||
versionNames: string[];
|
|
||||||
lastVersionName: string;
|
|
||||||
context: Pick<LoadContext, 'siteDir' | 'baseUrl' | 'i18n'>;
|
|
||||||
options: Pick<
|
|
||||||
PluginOptions,
|
|
||||||
| 'id'
|
|
||||||
| 'path'
|
|
||||||
| 'sidebarPath'
|
|
||||||
| 'routeBasePath'
|
|
||||||
| 'tagsBasePath'
|
|
||||||
| 'versions'
|
|
||||||
| 'editUrl'
|
|
||||||
| 'editCurrentVersion'
|
|
||||||
>;
|
|
||||||
}): VersionMetadata {
|
|
||||||
const {sidebarFilePath, contentPath, contentPathLocalized} =
|
|
||||||
getVersionMetadataPaths({versionName, context, options});
|
|
||||||
|
|
||||||
const isLast = versionName === lastVersionName;
|
|
||||||
|
|
||||||
// retro-compatible values
|
|
||||||
const defaultVersionLabel =
|
|
||||||
versionName === CURRENT_VERSION_NAME ? 'Next' : versionName;
|
|
||||||
function getDefaultVersionPathPart() {
|
|
||||||
if (isLast) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return versionName === CURRENT_VERSION_NAME ? 'next' : versionName;
|
|
||||||
}
|
|
||||||
const defaultVersionPathPart = getDefaultVersionPathPart();
|
|
||||||
|
|
||||||
const versionOptions = options.versions[versionName] ?? {};
|
|
||||||
|
|
||||||
const label = versionOptions.label ?? defaultVersionLabel;
|
|
||||||
const versionPathPart = versionOptions.path ?? defaultVersionPathPart;
|
|
||||||
|
|
||||||
const routePath = normalizeUrl([
|
|
||||||
context.baseUrl,
|
|
||||||
options.routeBasePath,
|
|
||||||
versionPathPart,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const versionEditUrls = getVersionEditUrls({
|
|
||||||
contentPath,
|
|
||||||
contentPathLocalized,
|
|
||||||
context,
|
|
||||||
options,
|
|
||||||
});
|
|
||||||
|
|
||||||
const routePriority = versionPathPart === '' ? -1 : undefined;
|
|
||||||
|
|
||||||
// the path that will be used to refer the docs tags
|
|
||||||
// example below will be using /docs/tags
|
|
||||||
const tagsPath = normalizeUrl([routePath, options.tagsBasePath]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
versionName,
|
|
||||||
label,
|
|
||||||
path: routePath,
|
|
||||||
tagsPath,
|
|
||||||
editUrl: versionEditUrls.editUrl,
|
|
||||||
editUrlLocalized: versionEditUrls.editUrlLocalized,
|
|
||||||
banner: getVersionBanner({
|
|
||||||
versionName,
|
|
||||||
versionNames,
|
|
||||||
lastVersionName,
|
|
||||||
options,
|
|
||||||
}),
|
|
||||||
badge: getVersionBadge({versionName, versionNames, options}),
|
|
||||||
className: getVersionClassName({versionName, options}),
|
|
||||||
isLast,
|
|
||||||
routePriority,
|
|
||||||
sidebarFilePath,
|
|
||||||
contentPath,
|
|
||||||
contentPathLocalized,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkVersionMetadataPaths({
|
|
||||||
versionMetadata,
|
|
||||||
context,
|
|
||||||
}: {
|
|
||||||
versionMetadata: VersionMetadata;
|
|
||||||
context: Pick<LoadContext, 'siteDir'>;
|
|
||||||
}) {
|
|
||||||
const {versionName, contentPath, sidebarFilePath} = versionMetadata;
|
|
||||||
const {siteDir} = context;
|
|
||||||
const isCurrentVersion = versionName === CURRENT_VERSION_NAME;
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(contentPath))) {
|
|
||||||
throw new Error(
|
|
||||||
`The docs folder does not exist for version "${versionName}". A docs folder is expected to be found at ${path.relative(
|
|
||||||
siteDir,
|
|
||||||
contentPath,
|
|
||||||
)}.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the current version defines a path to a sidebar file that does not
|
|
||||||
// exist, we throw! Note: for versioned sidebars, the file may not exist (as
|
|
||||||
// we prefer to not create it rather than to create an empty file)
|
|
||||||
// See https://github.com/facebook/docusaurus/issues/3366
|
|
||||||
// See https://github.com/facebook/docusaurus/pull/4775
|
|
||||||
if (
|
|
||||||
isCurrentVersion &&
|
|
||||||
typeof sidebarFilePath === 'string' &&
|
|
||||||
!(await fs.pathExists(sidebarFilePath))
|
|
||||||
) {
|
|
||||||
throw new Error(`The path to the sidebar file does not exist at "${path.relative(
|
|
||||||
siteDir,
|
|
||||||
sidebarFilePath,
|
|
||||||
)}".
|
|
||||||
Please set the docs "sidebarPath" field in your config file to:
|
|
||||||
- a sidebars path that exists
|
|
||||||
- false: to disable the sidebar
|
|
||||||
- undefined: for Docusaurus to generate it automatically`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO for retrocompatibility with existing behavior
|
|
||||||
// We should make this configurable
|
|
||||||
// "last version" is not a very good concept nor api surface
|
|
||||||
function getDefaultLastVersionName(versionNames: string[]) {
|
|
||||||
if (versionNames.length === 1) {
|
|
||||||
return versionNames[0]!;
|
|
||||||
}
|
|
||||||
return versionNames.filter(
|
|
||||||
(versionName) => versionName !== CURRENT_VERSION_NAME,
|
|
||||||
)[0]!;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 unknownVersionConfigNames = _.difference(
|
|
||||||
Object.keys(options.versions),
|
|
||||||
availableVersionNames,
|
|
||||||
);
|
|
||||||
if (unknownVersionConfigNames.length > 0) {
|
|
||||||
throw new Error(
|
|
||||||
`Invalid docs option "versions": unknown versions (${unknownVersionConfigNames.join(
|
|
||||||
',',
|
|
||||||
)}) found. ${availableVersionNamesMsg}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.onlyIncludeVersions) {
|
|
||||||
if (options.onlyIncludeVersions.length === 0) {
|
|
||||||
throw new Error(
|
|
||||||
`Invalid docs option "onlyIncludeVersions": an empty array is not allowed, at least one version is needed.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const unknownOnlyIncludeVersionNames = _.difference(
|
|
||||||
options.onlyIncludeVersions,
|
|
||||||
availableVersionNames,
|
|
||||||
);
|
|
||||||
if (unknownOnlyIncludeVersionNames.length > 0) {
|
|
||||||
throw new Error(
|
|
||||||
`Invalid docs option "onlyIncludeVersions": unknown versions (${unknownOnlyIncludeVersionNames.join(
|
|
||||||
',',
|
|
||||||
)}) found. ${availableVersionNamesMsg}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
options.lastVersion &&
|
|
||||||
!options.onlyIncludeVersions.includes(options.lastVersion)
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
`Invalid docs option "lastVersion": if you use both the "onlyIncludeVersions" and "lastVersion" options, then "lastVersion" must be present in the provided "onlyIncludeVersions" array.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter versions according to provided options.
|
|
||||||
* Note: we preserve the order in which versions are provided;
|
|
||||||
* the order of the onlyIncludeVersions array does not matter
|
|
||||||
*/
|
|
||||||
export function filterVersions(
|
|
||||||
versionNamesUnfiltered: string[],
|
|
||||||
options: Pick<PluginOptions, 'onlyIncludeVersions'>,
|
|
||||||
): string[] {
|
|
||||||
if (options.onlyIncludeVersions) {
|
|
||||||
return versionNamesUnfiltered.filter((name) =>
|
|
||||||
options.onlyIncludeVersions!.includes(name),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return versionNamesUnfiltered;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function readVersionsMetadata({
|
|
||||||
context,
|
|
||||||
options,
|
|
||||||
}: {
|
|
||||||
context: Pick<LoadContext, 'siteDir' | 'baseUrl' | 'i18n'>;
|
|
||||||
options: Pick<
|
|
||||||
PluginOptions,
|
|
||||||
| 'id'
|
|
||||||
| 'path'
|
|
||||||
| 'sidebarPath'
|
|
||||||
| 'routeBasePath'
|
|
||||||
| 'tagsBasePath'
|
|
||||||
| 'includeCurrentVersion'
|
|
||||||
| 'disableVersioning'
|
|
||||||
| 'lastVersion'
|
|
||||||
| 'versions'
|
|
||||||
| 'onlyIncludeVersions'
|
|
||||||
| 'editUrl'
|
|
||||||
| 'editCurrentVersion'
|
|
||||||
>;
|
|
||||||
}): Promise<VersionMetadata[]> {
|
|
||||||
const versionNamesUnfiltered = await readVersionNames(
|
|
||||||
context.siteDir,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
checkVersionsOptions(versionNamesUnfiltered, options);
|
|
||||||
|
|
||||||
const versionNames = filterVersions(versionNamesUnfiltered, options);
|
|
||||||
|
|
||||||
const lastVersionName =
|
|
||||||
options.lastVersion ?? getDefaultLastVersionName(versionNames);
|
|
||||||
|
|
||||||
const versionsMetadata = versionNames.map((versionName) =>
|
|
||||||
createVersionMetadata({
|
|
||||||
versionName,
|
|
||||||
versionNames,
|
|
||||||
lastVersionName,
|
|
||||||
context,
|
|
||||||
options,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
await Promise.all(
|
|
||||||
versionsMetadata.map((versionMetadata) =>
|
|
||||||
checkVersionMetadataPaths({versionMetadata, context}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return versionsMetadata;
|
|
||||||
}
|
|
|
@ -7,13 +7,8 @@
|
||||||
|
|
||||||
import {jest} from '@jest/globals';
|
import {jest} from '@jest/globals';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {
|
import {readVersionsMetadata} from '../index';
|
||||||
getVersionsFilePath,
|
import {DEFAULT_OPTIONS} from '../../options';
|
||||||
getVersionedDocsDirPath,
|
|
||||||
getVersionedSidebarsDirPath,
|
|
||||||
readVersionsMetadata,
|
|
||||||
} from '../versions';
|
|
||||||
import {DEFAULT_OPTIONS} from '../options';
|
|
||||||
import {DEFAULT_PLUGIN_ID} from '@docusaurus/utils';
|
import {DEFAULT_PLUGIN_ID} from '@docusaurus/utils';
|
||||||
import type {I18n} from '@docusaurus/types';
|
import type {I18n} from '@docusaurus/types';
|
||||||
import type {
|
import type {
|
||||||
|
@ -28,44 +23,11 @@ const DefaultI18N: I18n = {
|
||||||
localeConfigs: {},
|
localeConfigs: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('getVersionsFilePath', () => {
|
|
||||||
it('works', () => {
|
|
||||||
expect(getVersionsFilePath('someSiteDir', DEFAULT_PLUGIN_ID)).toBe(
|
|
||||||
`someSiteDir${path.sep}versions.json`,
|
|
||||||
);
|
|
||||||
expect(getVersionsFilePath('otherSite/dir', 'pluginId')).toBe(
|
|
||||||
`otherSite${path.sep}dir${path.sep}pluginId_versions.json`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getVersionedDocsDirPath', () => {
|
|
||||||
it('works', () => {
|
|
||||||
expect(getVersionedDocsDirPath('someSiteDir', DEFAULT_PLUGIN_ID)).toBe(
|
|
||||||
`someSiteDir${path.sep}versioned_docs`,
|
|
||||||
);
|
|
||||||
expect(getVersionedDocsDirPath('otherSite/dir', 'pluginId')).toBe(
|
|
||||||
`otherSite${path.sep}dir${path.sep}pluginId_versioned_docs`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getVersionedSidebarsDirPath', () => {
|
|
||||||
it('works', () => {
|
|
||||||
expect(getVersionedSidebarsDirPath('someSiteDir', DEFAULT_PLUGIN_ID)).toBe(
|
|
||||||
`someSiteDir${path.sep}versioned_sidebars`,
|
|
||||||
);
|
|
||||||
expect(getVersionedSidebarsDirPath('otherSite/dir', 'pluginId')).toBe(
|
|
||||||
`otherSite${path.sep}dir${path.sep}pluginId_versioned_sidebars`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('readVersionsMetadata', () => {
|
describe('readVersionsMetadata', () => {
|
||||||
describe('simple site', () => {
|
describe('simple site', () => {
|
||||||
async function loadSite() {
|
async function loadSite() {
|
||||||
const simpleSiteDir = path.resolve(
|
const simpleSiteDir = path.resolve(
|
||||||
path.join(__dirname, '__fixtures__', 'simple-site'),
|
path.join(__dirname, '../../__tests__/__fixtures__', 'simple-site'),
|
||||||
);
|
);
|
||||||
const defaultOptions: PluginOptions = {
|
const defaultOptions: PluginOptions = {
|
||||||
id: DEFAULT_PLUGIN_ID,
|
id: DEFAULT_PLUGIN_ID,
|
||||||
|
@ -217,7 +179,7 @@ describe('readVersionsMetadata', () => {
|
||||||
context: defaultContext,
|
context: defaultContext,
|
||||||
}),
|
}),
|
||||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||||
`"It is not possible to use docs without any version. Please check the configuration of these options: "includeCurrentVersion: false", "disableVersioning: false"."`,
|
`"It is not possible to use docs without any version. No version is included because you have requested to not include <PROJECT_ROOT>/docs through "includeCurrentVersion: false", while the versions file is empty/non-existent."`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -225,12 +187,12 @@ describe('readVersionsMetadata', () => {
|
||||||
describe('versioned site, pluginId=default', () => {
|
describe('versioned site, pluginId=default', () => {
|
||||||
async function loadSite() {
|
async function loadSite() {
|
||||||
const versionedSiteDir = path.resolve(
|
const versionedSiteDir = path.resolve(
|
||||||
path.join(__dirname, '__fixtures__', 'versioned-site'),
|
path.join(__dirname, '../../__tests__/__fixtures__', 'versioned-site'),
|
||||||
);
|
);
|
||||||
const defaultOptions: PluginOptions = {
|
const defaultOptions: PluginOptions = {
|
||||||
id: DEFAULT_PLUGIN_ID,
|
id: DEFAULT_PLUGIN_ID,
|
||||||
...DEFAULT_OPTIONS,
|
...DEFAULT_OPTIONS,
|
||||||
sidebarPath: 'sidebars.json',
|
sidebarPath: path.join(versionedSiteDir, 'sidebars.json'),
|
||||||
};
|
};
|
||||||
const defaultContext = {
|
const defaultContext = {
|
||||||
siteDir: versionedSiteDir,
|
siteDir: versionedSiteDir,
|
||||||
|
@ -562,7 +524,7 @@ describe('readVersionsMetadata', () => {
|
||||||
context: defaultContext,
|
context: defaultContext,
|
||||||
}),
|
}),
|
||||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||||
`"It is not possible to use docs without any version. Please check the configuration of these options: "includeCurrentVersion: false", "disableVersioning: true"."`,
|
`"It is not possible to use docs without any version. No version is included because you have requested to not include <PROJECT_ROOT>/docs through "includeCurrentVersion: false", while versioning is disabled with "disableVersioning: true"."`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -651,7 +613,9 @@ describe('readVersionsMetadata', () => {
|
||||||
options: defaultOptions,
|
options: defaultOptions,
|
||||||
context: defaultContext,
|
context: defaultContext,
|
||||||
}),
|
}),
|
||||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"Invalid version " "."`);
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Invalid version name " ": version name must contain at least one non-whitespace character."`,
|
||||||
|
);
|
||||||
jsonMock.mockRestore();
|
jsonMock.mockRestore();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -659,14 +623,14 @@ describe('readVersionsMetadata', () => {
|
||||||
describe('versioned site, pluginId=community', () => {
|
describe('versioned site, pluginId=community', () => {
|
||||||
async function loadSite() {
|
async function loadSite() {
|
||||||
const versionedSiteDir = path.resolve(
|
const versionedSiteDir = path.resolve(
|
||||||
path.join(__dirname, '__fixtures__', 'versioned-site'),
|
path.join(__dirname, '../../__tests__/__fixtures__', 'versioned-site'),
|
||||||
);
|
);
|
||||||
const defaultOptions: PluginOptions = {
|
const defaultOptions: PluginOptions = {
|
||||||
...DEFAULT_OPTIONS,
|
...DEFAULT_OPTIONS,
|
||||||
id: 'community',
|
id: 'community',
|
||||||
path: 'community',
|
path: 'community',
|
||||||
routeBasePath: 'communityBasePath',
|
routeBasePath: 'communityBasePath',
|
||||||
sidebarPath: 'sidebars.json',
|
sidebarPath: path.join(versionedSiteDir, 'sidebars.json'),
|
||||||
};
|
};
|
||||||
const defaultContext = {
|
const defaultContext = {
|
||||||
siteDir: versionedSiteDir,
|
siteDir: versionedSiteDir,
|
||||||
|
@ -779,7 +743,7 @@ describe('readVersionsMetadata', () => {
|
||||||
context: defaultContext,
|
context: defaultContext,
|
||||||
}),
|
}),
|
||||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||||
`"It is not possible to use docs without any version. Please check the configuration of these options: "includeCurrentVersion: false", "disableVersioning: true"."`,
|
`"It is not possible to use docs without any version. No version is included because you have requested to not include <PROJECT_ROOT>/community through "includeCurrentVersion: false", while versioning is disabled with "disableVersioning: true"."`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
220
packages/docusaurus-plugin-content-docs/src/versions/files.ts
Normal file
220
packages/docusaurus-plugin-content-docs/src/versions/files.ts
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
VERSIONS_JSON_FILE,
|
||||||
|
VERSIONED_DOCS_DIR,
|
||||||
|
VERSIONED_SIDEBARS_DIR,
|
||||||
|
CURRENT_VERSION_NAME,
|
||||||
|
} from '../constants';
|
||||||
|
import {validateVersionNames} from './validation';
|
||||||
|
import {getPluginI18nPath, DEFAULT_PLUGIN_ID} from '@docusaurus/utils';
|
||||||
|
import type {
|
||||||
|
PluginOptions,
|
||||||
|
VersionMetadata,
|
||||||
|
} from '@docusaurus/plugin-content-docs';
|
||||||
|
import type {VersionContext} from './index';
|
||||||
|
|
||||||
|
/** Add a prefix like `community_version-1.0.0`. No-op for default instance. */
|
||||||
|
function addPluginIdPrefix(fileOrDir: string, pluginId: string): string {
|
||||||
|
return pluginId === DEFAULT_PLUGIN_ID
|
||||||
|
? fileOrDir
|
||||||
|
: `${pluginId}_${fileOrDir}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `[siteDir]/community_versioned_docs/version-1.0.0` */
|
||||||
|
export function getVersionDocsDirPath(
|
||||||
|
siteDir: string,
|
||||||
|
pluginId: string,
|
||||||
|
versionName: string,
|
||||||
|
): string {
|
||||||
|
return path.join(
|
||||||
|
siteDir,
|
||||||
|
addPluginIdPrefix(VERSIONED_DOCS_DIR, pluginId),
|
||||||
|
`version-${versionName}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `[siteDir]/community_versioned_sidebars/version-1.0.0-sidebars.json` */
|
||||||
|
export function getVersionSidebarsPath(
|
||||||
|
siteDir: string,
|
||||||
|
pluginId: string,
|
||||||
|
versionName: string,
|
||||||
|
): string {
|
||||||
|
return path.join(
|
||||||
|
siteDir,
|
||||||
|
addPluginIdPrefix(VERSIONED_SIDEBARS_DIR, pluginId),
|
||||||
|
`version-${versionName}-sidebars.json`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDocsDirPathLocalized({
|
||||||
|
siteDir,
|
||||||
|
locale,
|
||||||
|
pluginId,
|
||||||
|
versionName,
|
||||||
|
}: {
|
||||||
|
siteDir: string;
|
||||||
|
locale: string;
|
||||||
|
pluginId: string;
|
||||||
|
versionName: string;
|
||||||
|
}): string {
|
||||||
|
return getPluginI18nPath({
|
||||||
|
siteDir,
|
||||||
|
locale,
|
||||||
|
pluginName: 'docusaurus-plugin-content-docs',
|
||||||
|
pluginId,
|
||||||
|
subPaths: [
|
||||||
|
versionName === CURRENT_VERSION_NAME
|
||||||
|
? CURRENT_VERSION_NAME
|
||||||
|
: `version-${versionName}`,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `community` => `[siteDir]/community_versions.json` */
|
||||||
|
export function getVersionsFilePath(siteDir: string, pluginId: string): string {
|
||||||
|
return path.join(siteDir, addPluginIdPrefix(VERSIONS_JSON_FILE, pluginId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the plugin's respective `versions.json` file, and returns its content.
|
||||||
|
*
|
||||||
|
* @throws Throws if validation fails, i.e. `versions.json` doesn't contain an
|
||||||
|
* array of valid version names.
|
||||||
|
*/
|
||||||
|
async function readVersionsFile(
|
||||||
|
siteDir: string,
|
||||||
|
pluginId: string,
|
||||||
|
): Promise<string[] | null> {
|
||||||
|
const versionsFilePath = getVersionsFilePath(siteDir, pluginId);
|
||||||
|
if (await fs.pathExists(versionsFilePath)) {
|
||||||
|
const content = await fs.readJSON(versionsFilePath);
|
||||||
|
validateVersionNames(content);
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the `versions.json` file, and returns an ordered list of version names.
|
||||||
|
*
|
||||||
|
* - If `disableVersioning` is turned on, it will return `["current"]` (requires
|
||||||
|
* `includeCurrentVersion` to be true);
|
||||||
|
* - If `includeCurrentVersion` is turned on, "current" will be inserted at the
|
||||||
|
* beginning, if not already there.
|
||||||
|
*
|
||||||
|
* You need to use {@link filterVersions} after this.
|
||||||
|
*
|
||||||
|
* @throws Throws an error if `disableVersioning: true` but `versions.json`
|
||||||
|
* doesn't exist (i.e. site is not versioned)
|
||||||
|
* @throws Throws an error if versions list is empty (empty `versions.json` or
|
||||||
|
* `disableVersioning` is true, and not including current version)
|
||||||
|
*/
|
||||||
|
export async function readVersionNames(
|
||||||
|
siteDir: string,
|
||||||
|
options: PluginOptions,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const versionFileContent = await readVersionsFile(siteDir, options.id);
|
||||||
|
|
||||||
|
if (!versionFileContent && options.disableVersioning) {
|
||||||
|
throw new Error(
|
||||||
|
`Docs: using "disableVersioning: true" 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; or
|
||||||
|
// - it's already 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. No version is included because you have requested to not include ${path.resolve(
|
||||||
|
options.path,
|
||||||
|
)} through "includeCurrentVersion: false", while ${
|
||||||
|
options.disableVersioning
|
||||||
|
? 'versioning is disabled with "disableVersioning: true"'
|
||||||
|
: `the versions file is empty/non-existent`
|
||||||
|
}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return versions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the path-related version metadata.
|
||||||
|
*
|
||||||
|
* @throws Throws if the resolved docs folder or sidebars file doesn't exist.
|
||||||
|
* Does not throw if a versioned sidebar is missing (since we don't create empty
|
||||||
|
* files).
|
||||||
|
*/
|
||||||
|
export async function getVersionMetadataPaths({
|
||||||
|
versionName,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
}: VersionContext): Promise<
|
||||||
|
Pick<
|
||||||
|
VersionMetadata,
|
||||||
|
'contentPath' | 'contentPathLocalized' | 'sidebarFilePath'
|
||||||
|
>
|
||||||
|
> {
|
||||||
|
const isCurrent = versionName === CURRENT_VERSION_NAME;
|
||||||
|
const contentPathLocalized = getDocsDirPathLocalized({
|
||||||
|
siteDir: context.siteDir,
|
||||||
|
locale: context.i18n.currentLocale,
|
||||||
|
pluginId: options.id,
|
||||||
|
versionName,
|
||||||
|
});
|
||||||
|
const contentPath = isCurrent
|
||||||
|
? path.resolve(context.siteDir, options.path)
|
||||||
|
: getVersionDocsDirPath(context.siteDir, options.id, versionName);
|
||||||
|
const sidebarFilePath = isCurrent
|
||||||
|
? options.sidebarPath
|
||||||
|
: getVersionSidebarsPath(context.siteDir, options.id, versionName);
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(contentPath))) {
|
||||||
|
throw new Error(
|
||||||
|
`The docs folder does not exist for version "${versionName}". A docs folder is expected to be found at ${path.relative(
|
||||||
|
context.siteDir,
|
||||||
|
contentPath,
|
||||||
|
)}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the current version defines a path to a sidebar file that does not
|
||||||
|
// exist, we throw! Note: for versioned sidebars, the file may not exist (as
|
||||||
|
// we prefer to not create it rather than to create an empty file)
|
||||||
|
// See https://github.com/facebook/docusaurus/issues/3366
|
||||||
|
// See https://github.com/facebook/docusaurus/pull/4775
|
||||||
|
if (
|
||||||
|
versionName === CURRENT_VERSION_NAME &&
|
||||||
|
typeof sidebarFilePath === 'string' &&
|
||||||
|
!(await fs.pathExists(sidebarFilePath))
|
||||||
|
) {
|
||||||
|
throw new Error(`The path to the sidebar file does not exist at "${path.relative(
|
||||||
|
context.siteDir,
|
||||||
|
sidebarFilePath,
|
||||||
|
)}".
|
||||||
|
Please set the docs "sidebarPath" field in your config file to:
|
||||||
|
- a sidebars path that exists
|
||||||
|
- false: to disable the sidebar
|
||||||
|
- undefined: for Docusaurus to generate it automatically`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {contentPath, contentPathLocalized, sidebarFilePath};
|
||||||
|
}
|
247
packages/docusaurus-plugin-content-docs/src/versions/index.ts
Normal file
247
packages/docusaurus-plugin-content-docs/src/versions/index.ts
Normal file
|
@ -0,0 +1,247 @@
|
||||||
|
/**
|
||||||
|
* 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 {CURRENT_VERSION_NAME} from '../constants';
|
||||||
|
import {normalizeUrl, posixPath} from '@docusaurus/utils';
|
||||||
|
import {validateVersionsOptions} from './validation';
|
||||||
|
import {
|
||||||
|
getDocsDirPathLocalized,
|
||||||
|
getVersionMetadataPaths,
|
||||||
|
readVersionNames,
|
||||||
|
} from './files';
|
||||||
|
import type {
|
||||||
|
PluginOptions,
|
||||||
|
VersionBanner,
|
||||||
|
VersionMetadata,
|
||||||
|
} from '@docusaurus/plugin-content-docs';
|
||||||
|
import type {LoadContext} from '@docusaurus/types';
|
||||||
|
|
||||||
|
export type VersionContext = {
|
||||||
|
/** The version name to get banner of. */
|
||||||
|
versionName: string;
|
||||||
|
/** All versions, ordered from newest to oldest. */
|
||||||
|
versionNames: string[];
|
||||||
|
lastVersionName: string;
|
||||||
|
context: LoadContext;
|
||||||
|
options: PluginOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getVersionEditUrls({
|
||||||
|
contentPath,
|
||||||
|
contentPathLocalized,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
}: Pick<VersionMetadata, 'contentPath' | 'contentPathLocalized'> & {
|
||||||
|
context: LoadContext;
|
||||||
|
options: PluginOptions;
|
||||||
|
}): Pick<VersionMetadata, 'editUrl' | 'editUrlLocalized'> {
|
||||||
|
// If the user is using the functional form of editUrl,
|
||||||
|
// she has total freedom and we can't compute a "version edit url"
|
||||||
|
if (!options.editUrl || typeof options.editUrl === 'function') {
|
||||||
|
return {editUrl: undefined, editUrlLocalized: undefined};
|
||||||
|
}
|
||||||
|
|
||||||
|
const editDirPath = options.editCurrentVersion ? options.path : contentPath;
|
||||||
|
const editDirPathLocalized = options.editCurrentVersion
|
||||||
|
? getDocsDirPathLocalized({
|
||||||
|
siteDir: context.siteDir,
|
||||||
|
locale: context.i18n.currentLocale,
|
||||||
|
versionName: CURRENT_VERSION_NAME,
|
||||||
|
pluginId: options.id,
|
||||||
|
})
|
||||||
|
: contentPathLocalized;
|
||||||
|
|
||||||
|
const versionPathSegment = posixPath(
|
||||||
|
path.relative(context.siteDir, path.resolve(context.siteDir, editDirPath)),
|
||||||
|
);
|
||||||
|
const versionPathSegmentLocalized = posixPath(
|
||||||
|
path.relative(
|
||||||
|
context.siteDir,
|
||||||
|
path.resolve(context.siteDir, editDirPathLocalized),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const editUrl = normalizeUrl([options.editUrl, versionPathSegment]);
|
||||||
|
|
||||||
|
const editUrlLocalized = normalizeUrl([
|
||||||
|
options.editUrl,
|
||||||
|
versionPathSegmentLocalized,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {editUrl, editUrlLocalized};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default version banner depends on the version's relative position to the
|
||||||
|
* latest version. More recent ones are "unreleased", and older ones are
|
||||||
|
* "unmaintained".
|
||||||
|
*/
|
||||||
|
export function getDefaultVersionBanner({
|
||||||
|
versionName,
|
||||||
|
versionNames,
|
||||||
|
lastVersionName,
|
||||||
|
}: VersionContext): VersionBanner | null {
|
||||||
|
// Current version: good, no banner
|
||||||
|
if (versionName === lastVersionName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Upcoming versions: unreleased banner
|
||||||
|
if (
|
||||||
|
versionNames.indexOf(versionName) < versionNames.indexOf(lastVersionName)
|
||||||
|
) {
|
||||||
|
return 'unreleased';
|
||||||
|
}
|
||||||
|
// Older versions: display unmaintained banner
|
||||||
|
return 'unmaintained';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVersionBanner(
|
||||||
|
context: VersionContext,
|
||||||
|
): VersionMetadata['banner'] {
|
||||||
|
const {versionName, options} = context;
|
||||||
|
const versionBannerOption = options.versions[versionName]?.banner;
|
||||||
|
if (versionBannerOption) {
|
||||||
|
return versionBannerOption === 'none' ? null : versionBannerOption;
|
||||||
|
}
|
||||||
|
return getDefaultVersionBanner(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVersionBadge({
|
||||||
|
versionName,
|
||||||
|
versionNames,
|
||||||
|
options,
|
||||||
|
}: VersionContext): VersionMetadata['badge'] {
|
||||||
|
// If site is not versioned or only one version is included
|
||||||
|
// we don't show the version badge by default
|
||||||
|
// See https://github.com/facebook/docusaurus/issues/3362
|
||||||
|
const defaultVersionBadge = versionNames.length !== 1;
|
||||||
|
return options.versions[versionName]?.badge ?? defaultVersionBadge;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVersionClassName({
|
||||||
|
versionName,
|
||||||
|
options,
|
||||||
|
}: VersionContext): VersionMetadata['className'] {
|
||||||
|
const defaultVersionClassName = `docs-version-${versionName}`;
|
||||||
|
return options.versions[versionName]?.className ?? defaultVersionClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVersionLabel({
|
||||||
|
versionName,
|
||||||
|
options,
|
||||||
|
}: VersionContext): VersionMetadata['label'] {
|
||||||
|
const defaultVersionLabel =
|
||||||
|
versionName === CURRENT_VERSION_NAME ? 'Next' : versionName;
|
||||||
|
return options.versions[versionName]?.label ?? defaultVersionLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVersionPathPart({
|
||||||
|
versionName,
|
||||||
|
options,
|
||||||
|
lastVersionName,
|
||||||
|
}: VersionContext): string {
|
||||||
|
function getDefaultVersionPathPart() {
|
||||||
|
if (versionName === lastVersionName) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return versionName === CURRENT_VERSION_NAME ? 'next' : versionName;
|
||||||
|
}
|
||||||
|
return options.versions[versionName]?.path ?? getDefaultVersionPathPart();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createVersionMetadata(
|
||||||
|
context: VersionContext,
|
||||||
|
): Promise<VersionMetadata> {
|
||||||
|
const {versionName, lastVersionName, options, context: loadContext} = context;
|
||||||
|
const {sidebarFilePath, contentPath, contentPathLocalized} =
|
||||||
|
await getVersionMetadataPaths(context);
|
||||||
|
const versionPathPart = getVersionPathPart(context);
|
||||||
|
|
||||||
|
const routePath = normalizeUrl([
|
||||||
|
loadContext.baseUrl,
|
||||||
|
options.routeBasePath,
|
||||||
|
versionPathPart,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const versionEditUrls = getVersionEditUrls({
|
||||||
|
contentPath,
|
||||||
|
contentPathLocalized,
|
||||||
|
context: loadContext,
|
||||||
|
options,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
versionName,
|
||||||
|
label: getVersionLabel(context),
|
||||||
|
banner: getVersionBanner(context),
|
||||||
|
badge: getVersionBadge(context),
|
||||||
|
className: getVersionClassName(context),
|
||||||
|
path: routePath,
|
||||||
|
tagsPath: normalizeUrl([routePath, options.tagsBasePath]),
|
||||||
|
...versionEditUrls,
|
||||||
|
isLast: versionName === lastVersionName,
|
||||||
|
routePriority: versionPathPart === '' ? -1 : undefined,
|
||||||
|
sidebarFilePath,
|
||||||
|
contentPath,
|
||||||
|
contentPathLocalized,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter versions according to provided options (i.e. `onlyIncludeVersions`).
|
||||||
|
*
|
||||||
|
* Note: we preserve the order in which versions are provided; the order of the
|
||||||
|
* `onlyIncludeVersions` array does not matter
|
||||||
|
*/
|
||||||
|
export function filterVersions(
|
||||||
|
versionNamesUnfiltered: string[],
|
||||||
|
options: PluginOptions,
|
||||||
|
): string[] {
|
||||||
|
if (options.onlyIncludeVersions) {
|
||||||
|
return versionNamesUnfiltered.filter((name) =>
|
||||||
|
options.onlyIncludeVersions!.includes(name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return versionNamesUnfiltered;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLastVersionName({
|
||||||
|
versionNames,
|
||||||
|
options,
|
||||||
|
}: Pick<VersionContext, 'versionNames' | 'options'>) {
|
||||||
|
return (
|
||||||
|
options.lastVersion ??
|
||||||
|
versionNames.find((name) => name !== CURRENT_VERSION_NAME) ??
|
||||||
|
CURRENT_VERSION_NAME
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readVersionsMetadata({
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
context: LoadContext;
|
||||||
|
options: PluginOptions;
|
||||||
|
}): Promise<VersionMetadata[]> {
|
||||||
|
const allVersionNames = await readVersionNames(context.siteDir, options);
|
||||||
|
validateVersionsOptions(allVersionNames, options);
|
||||||
|
const versionNames = filterVersions(allVersionNames, options);
|
||||||
|
const lastVersionName = getLastVersionName({versionNames, options});
|
||||||
|
const versionsMetadata = await Promise.all(
|
||||||
|
versionNames.map((versionName) =>
|
||||||
|
createVersionMetadata({
|
||||||
|
versionName,
|
||||||
|
versionNames,
|
||||||
|
lastVersionName,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return versionsMetadata;
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
/**
|
||||||
|
* 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 _ from 'lodash';
|
||||||
|
import type {VersionsOptions} from '@docusaurus/plugin-content-docs';
|
||||||
|
|
||||||
|
export function validateVersionName(name: unknown): asserts name is string {
|
||||||
|
if (typeof name !== 'string') {
|
||||||
|
throw new Error(
|
||||||
|
`Versions should be strings. Found type "${typeof name}" for version "${name}".`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!name.trim()) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid version name "${name}": version name must contain at least one non-whitespace character.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const errors: [RegExp, string][] = [
|
||||||
|
[/[/\\]/, 'should not include slash (/) or backslash (\\)'],
|
||||||
|
[/.{33,}/, 'cannot be longer than 32 characters'],
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
[/[<>:"|?*\x00-\x1F]/, 'should be a valid file path'],
|
||||||
|
[/^\.\.?$/, 'should not be "." or ".."'],
|
||||||
|
];
|
||||||
|
|
||||||
|
errors.forEach(([pattern, message]) => {
|
||||||
|
if (pattern.test(name)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid version name "${name}": version name ${message}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateVersionNames(
|
||||||
|
names: unknown,
|
||||||
|
): asserts names is string[] {
|
||||||
|
if (!Array.isArray(names)) {
|
||||||
|
throw new Error(
|
||||||
|
`The versions file should contain an array of version names! Found content: ${JSON.stringify(
|
||||||
|
names,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
names.forEach(validateVersionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Throws for one of the following invalid options:
|
||||||
|
* - `lastVersion` is non-existent
|
||||||
|
* - `versions` includes unknown keys
|
||||||
|
* - `onlyIncludeVersions` is empty, contains unknown names, or doesn't include
|
||||||
|
* `latestVersion` (if provided)
|
||||||
|
*/
|
||||||
|
export function validateVersionsOptions(
|
||||||
|
availableVersionNames: string[],
|
||||||
|
options: VersionsOptions,
|
||||||
|
): void {
|
||||||
|
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 unknownVersionConfigNames = _.difference(
|
||||||
|
Object.keys(options.versions),
|
||||||
|
availableVersionNames,
|
||||||
|
);
|
||||||
|
if (unknownVersionConfigNames.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid docs option "versions": unknown versions (${unknownVersionConfigNames.join(
|
||||||
|
',',
|
||||||
|
)}) found. ${availableVersionNamesMsg}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.onlyIncludeVersions) {
|
||||||
|
if (options.onlyIncludeVersions.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid docs option "onlyIncludeVersions": an empty array is not allowed, at least one version is needed.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const unknownOnlyIncludeVersionNames = _.difference(
|
||||||
|
options.onlyIncludeVersions,
|
||||||
|
availableVersionNames,
|
||||||
|
);
|
||||||
|
if (unknownOnlyIncludeVersionNames.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid docs option "onlyIncludeVersions": unknown versions (${unknownOnlyIncludeVersionNames.join(
|
||||||
|
',',
|
||||||
|
)}) found. ${availableVersionNamesMsg}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
options.lastVersion &&
|
||||||
|
!options.onlyIncludeVersions.includes(options.lastVersion)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid docs option "lastVersion": if you use both the "onlyIncludeVersions" and "lastVersion" options, then "lastVersion" must be present in the provided "onlyIncludeVersions" array.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue