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

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