/** * 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 { aliasedSitePath, getEditUrl, getFolderContainingFile, normalizeUrl, parseMarkdownString, posixPath, getDateTimeFormat, } from '@docusaurus/utils'; import {LoadContext} from '@docusaurus/types'; import {getFileLastUpdate} from './lastUpdate'; import { DocFile, DocMetadataBase, LastUpdateData, MetadataOptions, PluginOptions, VersionMetadata, } from './types'; import getSlug from './slug'; import {CURRENT_VERSION_NAME} from './constants'; import globby from 'globby'; import {getDocsDirPaths} from './versions'; type LastUpdateOptions = Pick< PluginOptions, 'showLastUpdateAuthor' | 'showLastUpdateTime' >; async function readLastUpdateData( filePath: string, options: LastUpdateOptions, ): 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 getFileLastUpdate(filePath) : { author: 'Author', timestamp: 1539502055, }; if (fileLastUpdateData) { const {author, timestamp} = fileLastUpdateData; return { lastUpdatedAt: showLastUpdateTime ? timestamp : undefined, lastUpdatedBy: showLastUpdateAuthor ? author : undefined, }; } } return {}; } export async function readDocFile( versionMetadata: Pick< VersionMetadata, 'docsDirPath' | 'docsDirPathLocalized' >, source: string, options: LastUpdateOptions, ): Promise<DocFile> { const docsDirPath = await getFolderContainingFile( getDocsDirPaths(versionMetadata), source, ); const filePath = path.join(docsDirPath, source); const [content, lastUpdate] = await Promise.all([ fs.readFile(filePath, 'utf-8'), readLastUpdateData(filePath, options), ]); return {source, content, lastUpdate, docsDirPath, filePath}; } export async function readVersionDocs( versionMetadata: VersionMetadata, options: Pick< PluginOptions, 'include' | 'showLastUpdateAuthor' | 'showLastUpdateTime' >, ): Promise<DocFile[]> { const sources = await globby(options.include, { cwd: versionMetadata.docsDirPath, }); return Promise.all( sources.map((source) => readDocFile(versionMetadata, source, options)), ); } export function processDocMetadata({ docFile, versionMetadata, context, options, }: { docFile: DocFile; versionMetadata: VersionMetadata; context: LoadContext; options: MetadataOptions; }): DocMetadataBase { const {source, content, lastUpdate, docsDirPath, filePath} = docFile; const {homePageId} = options; const {siteDir, i18n} = context; // ex: api/myDoc -> api // ex: myDoc -> . const docsFileDirName = path.dirname(source); const {frontMatter = {}, excerpt} = parseMarkdownString(content); const {sidebar_label, custom_edit_url} = frontMatter; const baseID: string = frontMatter.id || path.basename(source, path.extname(source)); if (baseID.includes('/')) { throw new Error(`Document id [${baseID}] cannot include "/".`); } // TODO legacy retrocompatibility // The same doc in 2 distinct version could keep the same id, // we just need to namespace the data by version const versionIdPart = versionMetadata.versionName === CURRENT_VERSION_NAME ? '' : `version-${versionMetadata.versionName}/`; // TODO legacy retrocompatibility // I think it's bad to affect the frontmatter id with the dirname const dirNameIdPart = docsFileDirName === '.' ? '' : `${docsFileDirName}/`; // TODO legacy composite id, requires a breaking change to modify this const id = `${versionIdPart}${dirNameIdPart}${baseID}`; const unversionedId = `${dirNameIdPart}${baseID}`; // TODO remove soon, deprecated homePageId const isDocsHomePage = unversionedId === (homePageId ?? '_index'); 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 choose either homePageId or slug, not both`, ); } const docSlug = isDocsHomePage ? '/' : getSlug({ baseID, dirName: docsFileDirName, frontmatterSlug: frontMatter.slug, }); // Default title is the id. const title: string = frontMatter.title || baseID; const description: string = frontMatter.description || excerpt; const permalink = normalizeUrl([versionMetadata.versionPath, docSlug]); function getDocEditUrl() { const relativeFilePath = path.relative(docsDirPath, filePath); if (typeof options.editUrl === 'function') { return options.editUrl({ version: versionMetadata.versionName, versionDocsDirPath: posixPath( path.relative(siteDir, versionMetadata.docsDirPath), ), docPath: posixPath(relativeFilePath), permalink, locale: context.i18n.currentLocale, }); } else if (typeof options.editUrl === 'string') { const isLocalized = docsDirPath === versionMetadata.docsDirPathLocalized; const baseVersionEditUrl = isLocalized && options.editLocalizedFiles ? versionMetadata.versionEditUrlLocalized : versionMetadata.versionEditUrl; return getEditUrl(relativeFilePath, baseVersionEditUrl); } else { return undefined; } } // Assign all of object properties during instantiation (if possible) for // NodeJS optimization. // Adding properties to object after instantiation will cause hidden // class transitions. return { unversionedId, id, isDocsHomePage, title, description, source: aliasedSitePath(filePath, siteDir), slug: docSlug, permalink, editUrl: custom_edit_url !== undefined ? custom_edit_url : getDocEditUrl(), version: versionMetadata.versionName, lastUpdatedBy: lastUpdate.lastUpdatedBy, lastUpdatedAt: lastUpdate.lastUpdatedAt, formattedLastUpdatedAt: lastUpdate.lastUpdatedAt ? getDateTimeFormat(i18n.currentLocale)(i18n.currentLocale).format( lastUpdate.lastUpdatedAt * 1000, ) : undefined, sidebar_label, }; }