/** * 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 fs from 'fs-extra'; import Yaml from 'js-yaml'; import path from 'path'; import {findAsyncSequential} from './index'; import type {ContentPaths} from './markdownLinks'; import logger from '@docusaurus/logger'; type DataFileParams = { /** Path to the potential data file, relative to `contentPaths` */ filePath: string; /** * Includes the base path and localized path, both of which are eligible for * sourcing data files. Both paths should be absolute. */ contentPaths: ContentPaths; }; /** * Looks for a data file in the potential content paths; loads a localized data * file in priority. * * @returns An absolute path to the data file, or `undefined` if there isn't one. */ export async function getDataFilePath({ filePath, contentPaths, }: DataFileParams): Promise { const contentPath = await findFolderContainingFile( getContentPathList(contentPaths), filePath, ); if (contentPath) { return path.resolve(contentPath, filePath); } return undefined; } /** * Looks up for a data file in the content paths, returns the object validated * and normalized according to the `validate` callback. * * @returns `undefined` when file not found * @throws Throws when validation fails, displaying a helpful context message. */ export async function getDataFileData( params: DataFileParams & { /** Used for the "The X file looks invalid" message. */ fileType: string; }, validate: (content: unknown) => T, ): Promise { const filePath = await getDataFilePath(params); if (!filePath) { return undefined; } try { const contentString = await fs.readFile(filePath, {encoding: 'utf8'}); const unsafeContent = Yaml.load(contentString); return validate(unsafeContent); } catch (err) { logger.error`The ${params.fileType} file at path=${filePath} looks invalid.`; throw err; } } /** * Takes the `contentPaths` data structure and returns an ordered path list * indicating their priorities. For all data, we look in the localized folder * in priority. */ export function getContentPathList(contentPaths: ContentPaths): string[] { return [contentPaths.contentPathLocalized, contentPaths.contentPath]; } /** * @param folderPaths a list of absolute paths. * @param relativeFilePath file path relative to each `folderPaths`. * @returns the first folder path in which the file exists, or `undefined` if * none is found. */ export async function findFolderContainingFile( folderPaths: string[], relativeFilePath: string, ): Promise { return findAsyncSequential(folderPaths, (folderPath) => fs.pathExists(path.join(folderPath, relativeFilePath)), ); } /** * Fail-fast alternative to `findFolderContainingFile`. * * @param folderPaths a list of absolute paths. * @param relativeFilePath file path relative to each `folderPaths`. * @returns the first folder path in which the file exists. * @throws Throws if no file can be found. You should use this method only when * you actually know the file exists (e.g. when the `relativeFilePath` is read * with a glob and you are just trying to localize it) */ export async function getFolderContainingFile( folderPaths: string[], relativeFilePath: string, ): Promise { const maybeFolderPath = await findFolderContainingFile( folderPaths, relativeFilePath, ); if (!maybeFolderPath) { throw new Error( `File "${relativeFilePath}" does not exist in any of these folders: - ${folderPaths.join('\n- ')}`, ); } return maybeFolderPath; }