docusaurus/packages/docusaurus-plugin-content-docs/src/metadata.ts
Sébastien Lorber f4434b2e42
feat(v2): absolute slugs and slug resolution system (#3084)
* rework slug to allow absolute slugs and slug resolution

* add slug metadata tests

* refactor docs metadata test + fix slug bugs

* fix tests

* fix docs tests failing due to randomness + update snapshot

* add test for addLeadingSlash
2020-07-21 18:26:30 +02:00

186 lines
4.9 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 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 "/".');
}
const id =
dirNameWithVersion !== '.' ? `${dirNameWithVersion}/${baseID}` : baseID;
const unversionedId = version ? removeVersionPrefix(id, version) : id;
const isDocsHomePage = unversionedId === homePageId;
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),
permalink,
editUrl: custom_edit_url !== undefined ? custom_edit_url : docsEditUrl,
version,
lastUpdatedBy,
lastUpdatedAt,
sidebar_label,
};
return metadata;
}