docusaurus/packages/docusaurus-plugin-content-docs/src/routes.ts
ozaki 0eb7b64aac
feat(docs, blog): add support for tags.yml, predefined list of tags (#10137)
Co-authored-by: Sébastien Lorber <slorber@users.noreply.github.com>
Co-authored-by: OzakIOne <OzakIOne@users.noreply.github.com>
Co-authored-by: sebastien <lorber.sebastien@gmail.com>
Co-authored-by: slorber <slorber@users.noreply.github.com>
2024-05-31 17:32:08 +02:00

263 lines
7.1 KiB
TypeScript

/**
* 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 logger from '@docusaurus/logger';
import {
docuHash,
normalizeUrl,
aliasedSitePathToRelativePath,
groupTaggedItems,
getTagVisibility,
} from '@docusaurus/utils';
import {
toTagDocListProp,
toTagsListTagsProp,
toVersionMetadataProp,
} from './props';
import type {
PluginContentLoadedActions,
RouteConfig,
RouteMetadata,
} from '@docusaurus/types';
import type {FullVersion, VersionTag, VersionTags} from './types';
import type {
CategoryGeneratedIndexMetadata,
DocMetadata,
PluginOptions,
} from '@docusaurus/plugin-content-docs';
function createDocRouteMetadata(docMeta: DocMetadata): RouteMetadata {
return {
sourceFilePath: aliasedSitePathToRelativePath(docMeta.source),
lastUpdatedAt: docMeta.lastUpdatedAt,
};
}
async function buildVersionCategoryGeneratedIndexRoutes({
version,
options,
}: BuildVersionRoutesParam): Promise<RouteConfig[]> {
async function buildCategoryGeneratedIndexRoute(
categoryGeneratedIndex: CategoryGeneratedIndexMetadata,
): Promise<RouteConfig> {
return {
path: categoryGeneratedIndex.permalink,
component: options.docCategoryGeneratedIndexComponent,
exact: true,
props: {
categoryGeneratedIndex,
},
// Same as doc, this sidebar route attribute permits to associate this
// subpage to the given sidebar
...(categoryGeneratedIndex.sidebar && {
sidebar: categoryGeneratedIndex.sidebar,
}),
};
}
return Promise.all(
version.categoryGeneratedIndices.map(buildCategoryGeneratedIndexRoute),
);
}
async function buildVersionDocRoutes({
version,
actions,
options,
}: BuildVersionRoutesParam): Promise<RouteConfig[]> {
return Promise.all(
version.docs.map(async (doc) => {
await actions.createData(
// Note that this created data path must be in sync with
// metadataPath provided to mdx-loader.
`${docuHash(doc.source)}.json`,
doc,
);
const docRoute: RouteConfig = {
path: doc.permalink,
component: options.docItemComponent,
exact: true,
modules: {
content: doc.source,
},
metadata: createDocRouteMetadata(doc),
// Because the parent (DocRoot) comp need to access it easily
// This permits to render the sidebar once without unmount/remount when
// navigating (and preserve sidebar state)
...(doc.sidebar && {
sidebar: doc.sidebar,
}),
};
return docRoute;
}),
);
}
async function buildVersionSidebarRoute(param: BuildVersionRoutesParam) {
const [docRoutes, categoryGeneratedIndexRoutes] = await Promise.all([
buildVersionDocRoutes(param),
buildVersionCategoryGeneratedIndexRoutes(param),
]);
const subRoutes = [...docRoutes, ...categoryGeneratedIndexRoutes];
return {
path: param.version.path,
exact: false,
component: param.options.docRootComponent,
routes: subRoutes,
};
}
function getVersionTags(docs: DocMetadata[]): VersionTags {
const groups = groupTaggedItems(docs, (doc) => doc.tags);
return _.mapValues(groups, ({tag, items: tagDocs}) => {
const tagVisibility = getTagVisibility({
items: tagDocs,
isUnlisted: (item) => item.unlisted,
});
return {
inline: tag.inline,
label: tag.label,
permalink: tag.permalink,
description: tag.description,
docIds: tagVisibility.listedItems.map((item) => item.id),
unlisted: tagVisibility.unlisted,
};
});
}
async function buildVersionTagsRoutes(
param: BuildVersionRoutesParam,
): Promise<RouteConfig[]> {
const {version, options} = param;
const versionTags = getVersionTags(version.docs);
async function buildTagsListRoute(): Promise<RouteConfig | null> {
const tags = toTagsListTagsProp(versionTags);
// Don't create a tags list page if there's no tag
if (tags.length === 0) {
return null;
}
return {
path: version.tagsPath,
exact: true,
component: options.docTagsListComponent,
props: {
tags,
},
};
}
async function buildTagDocListRoute(tag: VersionTag): Promise<RouteConfig> {
return {
path: tag.permalink,
component: options.docTagDocListComponent,
exact: true,
props: {
tag: toTagDocListProp({
allTagsPath: version.tagsPath,
tag,
docs: version.docs,
}),
},
};
}
const [tagsListRoute, allTagsDocListRoutes] = await Promise.all([
buildTagsListRoute(),
Promise.all(Object.values(versionTags).map(buildTagDocListRoute)),
]);
return _.compact([tagsListRoute, ...allTagsDocListRoutes]);
}
type BuildVersionRoutesParam = Omit<BuildAllRoutesParam, 'versions'> & {
version: FullVersion;
};
async function buildVersionRoutes(
param: BuildVersionRoutesParam,
): Promise<RouteConfig> {
const {version, options} = param;
async function buildVersionSubRoutes() {
const [sidebarRoute, tagsRoutes] = await Promise.all([
buildVersionSidebarRoute(param),
buildVersionTagsRoutes(param),
]);
return [sidebarRoute, ...tagsRoutes];
}
async function doBuildVersionRoutes(): Promise<RouteConfig> {
return {
path: version.path,
exact: false,
component: options.docVersionRootComponent,
routes: await buildVersionSubRoutes(),
props: {
// TODO Docusaurus v4 breaking change?
// expose version metadata as route context instead of props
version: toVersionMetadataProp(options.id, version),
},
priority: version.routePriority,
};
}
try {
return await doBuildVersionRoutes();
} catch (err) {
logger.error`Can't create version routes for version name=${version.versionName}`;
throw err;
}
}
type BuildAllRoutesParam = Omit<CreateAllRoutesParam, 'actions'> & {
actions: Omit<PluginContentLoadedActions, 'addRoute' | 'setGlobalData'>;
};
// TODO we want this buildAllRoutes function to be easily testable
// Ideally, we should avoid side effects here (ie not injecting actions)
export async function buildAllRoutes(
param: BuildAllRoutesParam,
): Promise<RouteConfig[]> {
const subRoutes = await Promise.all(
param.versions.map((version) =>
buildVersionRoutes({
...param,
version,
}),
),
);
// all docs routes are wrapped under a single parent route, this ensures
// the theme layout never unmounts/remounts when navigating between versions
return [
{
path: normalizeUrl([param.baseUrl, param.options.routeBasePath]),
exact: false,
component: param.options.docsRootComponent,
routes: subRoutes,
},
];
}
type CreateAllRoutesParam = {
baseUrl: string;
versions: FullVersion[];
options: PluginOptions;
actions: PluginContentLoadedActions;
aliasedSource: (str: string) => string;
};
export async function createAllRoutes(
param: CreateAllRoutesParam,
): Promise<void> {
const routes = await buildAllRoutes(param);
routes.forEach(param.actions.addRoute);
}