/** * 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 { normalizeUrl, docuHash, aliasedSitePath, getContentPathList, posixPath, addTrailingPathSeparator, createAbsoluteFilePathMatcher, resolveMarkdownLinkPathname, DEFAULT_PLUGIN_ID, } from '@docusaurus/utils'; import {getTagsFilePathsToWatch} from '@docusaurus/utils-validation'; import {createMDXLoaderRule} from '@docusaurus/mdx-loader'; import {resolveSidebarPathOption} from './sidebars'; import {CategoryMetadataFilenamePattern} from './sidebars/generator'; import {type DocEnv} from './docs'; import { getVersionFromSourceFilePath, readVersionsMetadata, toFullVersion, } from './versions/version'; import cliDocs from './cli'; import {VERSIONS_JSON_FILE} from './constants'; import {toGlobalDataVersion} from './globalData'; import { translateLoadedContent, getLoadedContentTranslationFiles, } from './translations'; import {createAllRoutes} from './routes'; import {createContentHelpers} from './contentHelpers'; import {loadVersion} from './versions/loadVersion'; import type { PluginOptions, VersionMetadata, DocFrontMatter, LoadedContent, } from '@docusaurus/plugin-content-docs'; import type {LoadContext, Plugin} from '@docusaurus/types'; import type {FullVersion} from './types'; import type {RuleSetRule} from 'webpack'; // MDX loader is not 100% deterministic, leading to cache invalidation issue // This permits to invalidate the MDX loader cache entries when content changes // Problem documented here: https://github.com/facebook/docusaurus/pull/10934 // TODO this is not a perfect solution, find better? async function createMdxLoaderDependencyFile({ dataDir, options, versionsMetadata, }: { dataDir: string; options: PluginOptions; versionsMetadata: VersionMetadata[]; }): Promise { // Disabled for unit tests, the side effect produces infinite watch loops // TODO find a better way :/ if (process.env.NODE_ENV === 'test') { return undefined; } const filePath = path.join(dataDir, '__mdx-loader-dependency.json'); // the cache is invalidated whenever this file content changes const fileContent = { options, versionsMetadata, }; await fs.ensureDir(dataDir); await fs.writeFile(filePath, JSON.stringify(fileContent)); return filePath; } export default async function pluginContentDocs( context: LoadContext, options: PluginOptions, ): Promise> { const {siteDir, generatedFilesDir, baseUrl, siteConfig} = context; // Mutate options to resolve sidebar path according to siteDir options.sidebarPath = resolveSidebarPathOption(siteDir, options.sidebarPath); const versionsMetadata = await readVersionsMetadata({context, options}); const pluginId = options.id; const pluginDataDirRoot = path.join( generatedFilesDir, 'docusaurus-plugin-content-docs', ); const dataDir = path.join(pluginDataDirRoot, pluginId); // TODO Docusaurus v4 breaking change // module aliasing should be automatic // we should never find local absolute FS paths in the codegen registry const aliasedSource = (source: string) => `~docs/${posixPath(path.relative(pluginDataDirRoot, source))}`; // TODO env should be injected into all plugins const env = process.env.NODE_ENV as DocEnv; const contentHelpers = createContentHelpers(); async function createDocsMDXLoaderRule(): Promise { const { rehypePlugins, remarkPlugins, recmaPlugins, beforeDefaultRehypePlugins, beforeDefaultRemarkPlugins, } = options; const contentDirs = versionsMetadata .flatMap(getContentPathList) // Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970 .map(addTrailingPathSeparator); return createMDXLoaderRule({ include: contentDirs, options: { dependencies: [ await createMdxLoaderDependencyFile({ dataDir, options, versionsMetadata, }), ].filter((d): d is string => typeof d === 'string'), useCrossCompilerCache: siteConfig.future.experimental_faster.mdxCrossCompilerCache, admonitions: options.admonitions, remarkPlugins, rehypePlugins, recmaPlugins, beforeDefaultRehypePlugins, beforeDefaultRemarkPlugins, staticDirs: siteConfig.staticDirectories.map((dir) => path.resolve(siteDir, dir), ), siteDir, isMDXPartial: createAbsoluteFilePathMatcher( options.exclude, contentDirs, ), metadataPath: (mdxPath: string) => { // Note that metadataPath must be the same/in-sync as // the path from createData for each MDX. const aliasedPath = aliasedSitePath(mdxPath, siteDir); return path.join(dataDir, `${docuHash(aliasedPath)}.json`); }, // createAssets converts relative paths to require() calls createAssets: ({frontMatter}: {frontMatter: DocFrontMatter}) => ({ image: frontMatter.image, }), markdownConfig: siteConfig.markdown, resolveMarkdownLink: ({linkPathname, sourceFilePath}) => { const version = getVersionFromSourceFilePath( sourceFilePath, versionsMetadata, ); return resolveMarkdownLinkPathname(linkPathname, { sourceFilePath, sourceToPermalink: contentHelpers.sourceToPermalink, siteDir, contentPaths: version, }); }, }, }); } const docsMDXLoaderRule = await createDocsMDXLoaderRule(); return { name: 'docusaurus-plugin-content-docs', extendCli(cli) { const isDefaultPluginId = pluginId === DEFAULT_PLUGIN_ID; // Need to create one distinct command per plugin instance // otherwise 2 instances would try to execute the command! const command = isDefaultPluginId ? 'docs:version' : `docs:version:${pluginId}`; const commandDescription = isDefaultPluginId ? 'Tag a new docs version' : `Tag a new docs version (${pluginId})`; cli .command(command) .arguments('') .description(commandDescription) .action((version: unknown) => cliDocs.cliDocsVersionCommand(version, options, context), ); }, getTranslationFiles({content}) { return getLoadedContentTranslationFiles(content); }, getPathsToWatch() { function getVersionPathsToWatch(version: VersionMetadata): string[] { const result = [ ...options.include.flatMap((pattern) => getContentPathList(version).map( (docsDirPath) => `${docsDirPath}/${pattern}`, ), ), ...getTagsFilePathsToWatch({ contentPaths: version, tags: options.tags, }), `${version.contentPath}/**/${CategoryMetadataFilenamePattern}`, ]; if (typeof version.sidebarFilePath === 'string') { result.unshift(version.sidebarFilePath); } return result; } return versionsMetadata.flatMap(getVersionPathsToWatch); }, async loadContent() { return { loadedVersions: await Promise.all( versionsMetadata.map((versionMetadata) => loadVersion({ context, options, env, versionMetadata, }), ), ), }; }, translateContent({content, translationFiles}) { return translateLoadedContent(content, translationFiles); }, async contentLoaded({content, actions}) { contentHelpers.updateContent(content); const versions: FullVersion[] = content.loadedVersions.map(toFullVersion); await createAllRoutes({ baseUrl, versions, options, actions, aliasedSource, }); actions.setGlobalData({ path: normalizeUrl([baseUrl, options.routeBasePath]), versions: versions.map(toGlobalDataVersion), breadcrumbs: options.breadcrumbs, }); }, configureWebpack() { return { ignoreWarnings: [ // Suppress warnings about non-existing of versions file. (e) => e.message.includes("Can't resolve") && e.message.includes(VERSIONS_JSON_FILE), ], resolve: { alias: { '~docs': pluginDataDirRoot, }, }, module: { rules: [docsMDXLoaderRule], }, }; }, }; } export {validateOptions} from './options';