diff --git a/packages/docusaurus-mdx-loader/src/createMDXLoader.ts b/packages/docusaurus-mdx-loader/src/createMDXLoader.ts new file mode 100644 index 0000000000..523c6d2401 --- /dev/null +++ b/packages/docusaurus-mdx-loader/src/createMDXLoader.ts @@ -0,0 +1,48 @@ +/** + * 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 {createProcessors} from './processor'; +import type {Options} from './loader'; +import type {RuleSetRule, RuleSetUseItem} from 'webpack'; + +async function enhancedOptions(options: Options): Promise { + // Because Jest doesn't like ESM / createProcessors() + if (process.env.N0DE_ENV === 'test' || process.env.JEST_WORKER_ID) { + return options; + } + + // We create the processor earlier here, to avoid the lazy processor creating + // Lazy creation messes-up with Rsdoctor ability to measure mdx-loader perf + const newOptions: Options = options.processors + ? options + : {...options, processors: await createProcessors({options})}; + + return newOptions; +} + +export async function createMDXLoaderItem( + options: Options, +): Promise { + return { + loader: require.resolve('@docusaurus/mdx-loader'), + options: await enhancedOptions(options), + }; +} + +export async function createMDXLoaderRule({ + include, + options, +}: { + include: RuleSetRule['include']; + options: Options; +}): Promise { + return { + test: /\.mdx?$/i, + include, + use: [await createMDXLoaderItem(options)], + }; +} diff --git a/packages/docusaurus-mdx-loader/src/index.ts b/packages/docusaurus-mdx-loader/src/index.ts index 41fbf60da4..d8e5ffa2e9 100644 --- a/packages/docusaurus-mdx-loader/src/index.ts +++ b/packages/docusaurus-mdx-loader/src/index.ts @@ -9,6 +9,8 @@ import {mdxLoader} from './loader'; import type {TOCItem as TOCItemImported} from './remark/toc/types'; +export {createMDXLoaderRule, createMDXLoaderItem} from './createMDXLoader'; + export default mdxLoader; export type TOCItem = TOCItemImported; diff --git a/packages/docusaurus-mdx-loader/src/loader.ts b/packages/docusaurus-mdx-loader/src/loader.ts index f84c8ef1ee..71a9fe992e 100644 --- a/packages/docusaurus-mdx-loader/src/loader.ts +++ b/packages/docusaurus-mdx-loader/src/loader.ts @@ -5,20 +5,25 @@ * LICENSE file in the root directory of this source tree. */ -import fs from 'fs-extra'; import logger from '@docusaurus/logger'; import { DEFAULT_PARSE_FRONT_MATTER, - escapePath, getFileLoaderUtils, getWebpackLoaderCompilerName, } from '@docusaurus/utils'; import stringifyObject from 'stringify-object'; -import preprocessor from './preprocessor'; -import {validateMDXFrontMatter} from './frontMatter'; -import {createProcessorCached} from './processor'; +import { + compileToJSX, + createAssetsExportCode, + extractContentTitleData, + readMetadataPath, +} from './utils'; +import type { + SimpleProcessors, + MDXOptions, + SimpleProcessorResult, +} from './processor'; import type {ResolveMarkdownLink} from './remark/resolveMarkdownLinks'; -import type {MDXOptions} from './processor'; import type {MarkdownConfig} from '@docusaurus/types'; import type {LoaderContext} from 'webpack'; @@ -43,98 +48,11 @@ export type Options = Partial & { metadata: {[key: string]: unknown}; }) => {[key: string]: unknown}; resolveMarkdownLink?: ResolveMarkdownLink; + + // Will usually be created by "createMDXLoaderItem" + processors?: SimpleProcessors; }; -/** - * When this throws, it generally means that there's no metadata file associated - * with this MDX document. It can happen when using MDX partials (usually - * starting with _). That's why it's important to provide the `isMDXPartial` - * function in config - */ -async function readMetadataPath(metadataPath: string) { - try { - return await fs.readFile(metadataPath, 'utf8'); - } catch (err) { - logger.error`MDX loader can't read MDX metadata file path=${metadataPath}. Maybe the isMDXPartial option function was not provided?`; - throw err; - } -} - -/** - * Converts assets an object with Webpack require calls code. - * This is useful for mdx files to reference co-located assets using relative - * paths. Those assets should enter the Webpack assets pipeline and be hashed. - * For now, we only handle that for images and paths starting with `./`: - * - * `{image: "./myImage.png"}` => `{image: require("./myImage.png")}` - */ -function createAssetsExportCode({ - assets, - inlineMarkdownAssetImageFileLoader, -}: { - assets: unknown; - inlineMarkdownAssetImageFileLoader: string; -}) { - if ( - typeof assets !== 'object' || - !assets || - Object.keys(assets).length === 0 - ) { - return 'undefined'; - } - - // TODO implementation can be completed/enhanced - function createAssetValueCode(assetValue: unknown): string | undefined { - if (Array.isArray(assetValue)) { - const arrayItemCodes = assetValue.map( - (item: unknown) => createAssetValueCode(item) ?? 'undefined', - ); - return `[${arrayItemCodes.join(', ')}]`; - } - // Only process string values starting with ./ - // We could enhance this logic and check if file exists on disc? - if (typeof assetValue === 'string' && assetValue.startsWith('./')) { - // TODO do we have other use-cases than image assets? - // Probably not worth adding more support, as we want to move to Webpack 5 new asset system (https://github.com/facebook/docusaurus/pull/4708) - return `require("${inlineMarkdownAssetImageFileLoader}${escapePath( - assetValue, - )}").default`; - } - return undefined; - } - - const assetEntries = Object.entries(assets); - - const codeLines = assetEntries - .map(([key, value]: [string, unknown]) => { - const assetRequireCode = createAssetValueCode(value); - return assetRequireCode ? `"${key}": ${assetRequireCode},` : undefined; - }) - .filter(Boolean); - - return `{\n${codeLines.join('\n')}\n}`; -} - -// TODO temporary, remove this after v3.1? -// Some plugin authors use our mdx-loader, despite it not being public API -// see https://github.com/facebook/docusaurus/issues/8298 -function ensureMarkdownConfig(reqOptions: Options) { - if (!reqOptions.markdownConfig) { - throw new Error( - 'Docusaurus v3+ requires MDX loader options.markdownConfig - plugin authors using the MDX loader should make sure to provide that option', - ); - } -} - -/** - * data.contentTitle is set by the remark contentTitle plugin - */ -function extractContentTitleData(data: { - [key: string]: unknown; -}): string | undefined { - return data.contentTitle as string | undefined; -} - export async function mdxLoader( this: LoaderContext, fileContent: string, @@ -144,59 +62,25 @@ export async function mdxLoader( const filePath = this.resourcePath; const options: Options = this.getOptions(); - ensureMarkdownConfig(options); - const {frontMatter} = await options.markdownConfig.parseFrontMatter({ filePath, fileContent, defaultParseFrontMatter: DEFAULT_PARSE_FRONT_MATTER, }); - const mdxFrontMatter = validateMDXFrontMatter(frontMatter.mdx); - - const preprocessedContent = preprocessor({ - fileContent, - filePath, - admonitions: options.admonitions, - markdownConfig: options.markdownConfig, - }); const hasFrontMatter = Object.keys(frontMatter).length > 0; - const processor = await createProcessorCached({ - filePath, - options, - mdxFrontMatter, - }); - - let result: {content: string; data: {[key: string]: unknown}}; + let result: SimpleProcessorResult; try { - result = await processor.process({ - content: preprocessedContent, + result = await compileToJSX({ + fileContent, filePath, frontMatter, + options, compilerName, }); - } catch (errorUnknown) { - const error = errorUnknown as Error; - - // MDX can emit errors that have useful extra attributes - const errorJSON = JSON.stringify(error, null, 2); - const errorDetails = - errorJSON === '{}' - ? // regular JS error case: print stacktrace - error.stack ?? 'N/A' - : // MDX error: print extra attributes + stacktrace - `${errorJSON}\n${error.stack}`; - - return callback( - new Error( - `MDX compilation failed for file ${logger.path(filePath)}\nCause: ${ - error.message - }\nDetails:\n${errorDetails}`, - // TODO error cause doesn't seem to be used by Webpack stats.errors :s - {cause: error}, - ), - ); + } catch (error) { + return callback(error as Error); } const contentTitle = extractContentTitleData(result.data); diff --git a/packages/docusaurus-mdx-loader/src/processor.ts b/packages/docusaurus-mdx-loader/src/processor.ts index 1a89d2f6ee..09f76bab32 100644 --- a/packages/docusaurus-mdx-loader/src/processor.ts +++ b/packages/docusaurus-mdx-loader/src/processor.ts @@ -31,10 +31,13 @@ import type {ProcessorOptions} from '@mdx-js/mdx'; // See https://github.com/microsoft/TypeScript/issues/49721#issuecomment-1517839391 type Pluggable = any; // TODO fix this asap -type SimpleProcessorResult = {content: string; data: {[key: string]: unknown}}; +export type SimpleProcessorResult = { + content: string; + data: {[key: string]: unknown}; +}; // TODO alt interface because impossible to import type Processor (ESM + TS :/) -type SimpleProcessor = { +export type SimpleProcessor = { process: ({ content, filePath, @@ -219,28 +222,22 @@ export async function createProcessorUncached(parameters: { } // We use different compilers depending on the file type (md vs mdx) -type ProcessorsCacheEntry = { +export type SimpleProcessors = { mdProcessor: SimpleProcessor; mdxProcessor: SimpleProcessor; }; // Compilers are cached so that Remark/Rehype plugins can run // expensive code during initialization -const ProcessorsCache = new Map(); +const ProcessorsCache = new Map(); -async function createProcessorsCacheEntry({ +export async function createProcessors({ options, }: { options: Options; -}): Promise { +}): Promise { const {createProcessorSync} = await createProcessorFactory(); - - const compilers = ProcessorsCache.get(options); - if (compilers) { - return compilers; - } - - const compilerCacheEntry: ProcessorsCacheEntry = { + return { mdProcessor: createProcessorSync({ options, format: 'md', @@ -250,13 +247,23 @@ async function createProcessorsCacheEntry({ format: 'mdx', }), }; - - ProcessorsCache.set(options, compilerCacheEntry); - - return compilerCacheEntry; } -export async function createProcessorCached({ +async function createProcessorsCacheEntry({ + options, +}: { + options: Options; +}): Promise { + const compilers = ProcessorsCache.get(options); + if (compilers) { + return compilers; + } + const processors = await createProcessors({options}); + ProcessorsCache.set(options, processors); + return processors; +} + +export async function getProcessor({ filePath, mdxFrontMatter, options, @@ -265,7 +272,8 @@ export async function createProcessorCached({ mdxFrontMatter: MDXFrontMatter; options: Options; }): Promise { - const compilers = await createProcessorsCacheEntry({options}); + const processors = + options.processors ?? (await createProcessorsCacheEntry({options})); const format = getFormat({ filePath, @@ -273,5 +281,5 @@ export async function createProcessorCached({ markdownConfigFormat: options.markdownConfig.format, }); - return format === 'md' ? compilers.mdProcessor : compilers.mdxProcessor; + return format === 'md' ? processors.mdProcessor : processors.mdxProcessor; } diff --git a/packages/docusaurus-mdx-loader/src/utils.ts b/packages/docusaurus-mdx-loader/src/utils.ts new file mode 100644 index 0000000000..94a17f6d09 --- /dev/null +++ b/packages/docusaurus-mdx-loader/src/utils.ts @@ -0,0 +1,152 @@ +/** + * 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 logger from '@docusaurus/logger'; +import {escapePath, type WebpackCompilerName} from '@docusaurus/utils'; +import {getProcessor, type SimpleProcessorResult} from './processor'; +import {validateMDXFrontMatter} from './frontMatter'; +import preprocessor from './preprocessor'; +import type {Options} from './loader'; + +/** + * When this throws, it generally means that there's no metadata file associated + * with this MDX document. It can happen when using MDX partials (usually + * starting with _). That's why it's important to provide the `isMDXPartial` + * function in config + */ +export async function readMetadataPath(metadataPath: string): Promise { + try { + return await fs.readFile(metadataPath, 'utf8'); + } catch (error) { + throw new Error( + logger.interpolate`MDX loader can't read MDX metadata file path=${metadataPath}. Maybe the isMDXPartial option function was not provided?`, + {cause: error as Error}, + ); + } +} + +/** + * Converts assets an object with Webpack require calls code. + * This is useful for mdx files to reference co-located assets using relative + * paths. Those assets should enter the Webpack assets pipeline and be hashed. + * For now, we only handle that for images and paths starting with `./`: + * + * `{image: "./myImage.png"}` => `{image: require("./myImage.png")}` + */ +export function createAssetsExportCode({ + assets, + inlineMarkdownAssetImageFileLoader, +}: { + assets: unknown; + inlineMarkdownAssetImageFileLoader: string; +}): string { + if ( + typeof assets !== 'object' || + !assets || + Object.keys(assets).length === 0 + ) { + return 'undefined'; + } + + // TODO implementation can be completed/enhanced + function createAssetValueCode(assetValue: unknown): string | undefined { + if (Array.isArray(assetValue)) { + const arrayItemCodes = assetValue.map( + (item: unknown) => createAssetValueCode(item) ?? 'undefined', + ); + return `[${arrayItemCodes.join(', ')}]`; + } + // Only process string values starting with ./ + // We could enhance this logic and check if file exists on disc? + if (typeof assetValue === 'string' && assetValue.startsWith('./')) { + // TODO do we have other use-cases than image assets? + // Probably not worth adding more support, as we want to move to Webpack 5 new asset system (https://github.com/facebook/docusaurus/pull/4708) + return `require("${inlineMarkdownAssetImageFileLoader}${escapePath( + assetValue, + )}").default`; + } + return undefined; + } + + const assetEntries = Object.entries(assets); + + const codeLines = assetEntries + .map(([key, value]: [string, unknown]) => { + const assetRequireCode = createAssetValueCode(value); + return assetRequireCode ? `"${key}": ${assetRequireCode},` : undefined; + }) + .filter(Boolean); + + return `{\n${codeLines.join('\n')}\n}`; +} + +/** + * data.contentTitle is set by the remark contentTitle plugin + */ +export function extractContentTitleData(data: { + [key: string]: unknown; +}): string | undefined { + return data.contentTitle as string | undefined; +} + +export async function compileToJSX({ + filePath, + fileContent, + frontMatter, + options, + compilerName, +}: { + filePath: string; + fileContent: string; + frontMatter: Record; + options: Options; + compilerName: WebpackCompilerName; +}): Promise { + const preprocessedFileContent = preprocessor({ + fileContent, + filePath, + admonitions: options.admonitions, + markdownConfig: options.markdownConfig, + }); + + const mdxFrontMatter = validateMDXFrontMatter(frontMatter.mdx); + + const processor = await getProcessor({ + filePath, + options, + mdxFrontMatter, + }); + + try { + return await processor.process({ + content: preprocessedFileContent, + filePath, + frontMatter, + compilerName, + }); + } catch (errorUnknown) { + const error = errorUnknown as Error; + + // MDX can emit errors that have useful extra attributes + const errorJSON = JSON.stringify(error, null, 2); + const errorDetails = + errorJSON === '{}' + ? // regular JS error case: print stacktrace + error.stack ?? 'N/A' + : // MDX error: print extra attributes + stacktrace + `${errorJSON}\n${error.stack}`; + + throw new Error( + `MDX compilation failed for file ${logger.path(filePath)}\nCause: ${ + error.message + }\nDetails:\n${errorDetails}`, + // TODO error cause doesn't seem to be used by Webpack stats.errors :s + {cause: error}, + ); + } +} diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts index b5ffcb7571..7c9247822d 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts @@ -107,6 +107,7 @@ const getPlugin = async ( url: 'https://docusaurus.io', markdown, future: {}, + staticDirectories: ['static'], } as DocusaurusConfig; return pluginContentBlog( { diff --git a/packages/docusaurus-plugin-content-blog/src/index.ts b/packages/docusaurus-plugin-content-blog/src/index.ts index 7ae3b5a54c..e40cfeab61 100644 --- a/packages/docusaurus-plugin-content-blog/src/index.ts +++ b/packages/docusaurus-plugin-content-blog/src/index.ts @@ -22,6 +22,10 @@ import { type SourceToPermalink, } from '@docusaurus/utils'; import {getTagsFilePathsToWatch} from '@docusaurus/utils-validation'; +import { + createMDXLoaderItem, + type Options as MDXLoaderOptions, +} from '@docusaurus/mdx-loader'; import { getBlogTags, paginateBlogPosts, @@ -47,8 +51,7 @@ import type { BlogContent, BlogPaginated, } from '@docusaurus/plugin-content-blog'; -import type {Options as MDXLoaderOptions} from '@docusaurus/mdx-loader/lib/loader'; -import type {RuleSetUseItem} from 'webpack'; +import type {RuleSetRule, RuleSetUseItem} from 'webpack'; const PluginName = 'docusaurus-plugin-content-blog'; @@ -127,6 +130,97 @@ export default async function pluginContentBlog( const sourceToPermalinkHelper = createSourceToPermalinkHelper(); + async function createBlogMDXLoaderRule(): Promise { + const { + admonitions, + rehypePlugins, + remarkPlugins, + recmaPlugins, + truncateMarker, + beforeDefaultRemarkPlugins, + beforeDefaultRehypePlugins, + } = options; + + const contentDirs = getContentPathList(contentPaths); + + const loaderOptions: MDXLoaderOptions = { + admonitions, + remarkPlugins, + rehypePlugins, + recmaPlugins, + beforeDefaultRemarkPlugins: [ + footnoteIDFixer, + ...beforeDefaultRemarkPlugins, + ], + beforeDefaultRehypePlugins, + 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`); + }, + // For blog posts a title in markdown is always removed + // Blog posts title are rendered separately + removeContentTitle: true, + // Assets allow to convert some relative images paths to + // require() calls + // @ts-expect-error: TODO fix typing issue + createAssets: ({ + frontMatter, + metadata, + }: { + frontMatter: BlogPostFrontMatter; + metadata: BlogPostMetadata; + }): Assets => ({ + image: frontMatter.image, + authorsImageUrls: metadata.authors.map((author) => author.imageURL), + }), + markdownConfig: siteConfig.markdown, + resolveMarkdownLink: ({linkPathname, sourceFilePath}) => { + const permalink = resolveMarkdownLinkPathname(linkPathname, { + sourceFilePath, + sourceToPermalink: sourceToPermalinkHelper.get(), + siteDir, + contentPaths, + }); + if (permalink === null) { + logger.report( + onBrokenMarkdownLinks, + )`Blog markdown link couldn't be resolved: (url=${linkPathname}) in source file path=${sourceFilePath}`; + } + return permalink; + }, + }; + + function createBlogMarkdownLoader(): RuleSetUseItem { + const markdownLoaderOptions: BlogMarkdownLoaderOptions = { + truncateMarker, + }; + return { + loader: path.resolve(__dirname, './markdownLoader.js'), + options: markdownLoaderOptions, + }; + } + + return { + test: /\.mdx?$/i, + include: contentDirs + // Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970 + .map(addTrailingPathSeparator), + use: [ + await createMDXLoaderItem(loaderOptions), + createBlogMarkdownLoader(), + ], + }; + } + + const blogMDXLoaderRule = await createBlogMDXLoaderRule(); + return { name: PluginName, @@ -273,91 +367,6 @@ export default async function pluginContentBlog( }, configureWebpack() { - const { - admonitions, - rehypePlugins, - remarkPlugins, - recmaPlugins, - truncateMarker, - beforeDefaultRemarkPlugins, - beforeDefaultRehypePlugins, - } = options; - - const contentDirs = getContentPathList(contentPaths); - - function createMDXLoader(): RuleSetUseItem { - const loaderOptions: MDXLoaderOptions = { - admonitions, - remarkPlugins, - rehypePlugins, - recmaPlugins, - beforeDefaultRemarkPlugins: [ - footnoteIDFixer, - ...beforeDefaultRemarkPlugins, - ], - beforeDefaultRehypePlugins, - 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`); - }, - // For blog posts a title in markdown is always removed - // Blog posts title are rendered separately - removeContentTitle: true, - // Assets allow to convert some relative images paths to - // require() calls - // @ts-expect-error: TODO fix typing issue - createAssets: ({ - frontMatter, - metadata, - }: { - frontMatter: BlogPostFrontMatter; - metadata: BlogPostMetadata; - }): Assets => ({ - image: frontMatter.image, - authorsImageUrls: metadata.authors.map((author) => author.imageURL), - }), - markdownConfig: siteConfig.markdown, - resolveMarkdownLink: ({linkPathname, sourceFilePath}) => { - const permalink = resolveMarkdownLinkPathname(linkPathname, { - sourceFilePath, - sourceToPermalink: sourceToPermalinkHelper.get(), - siteDir, - contentPaths, - }); - if (permalink === null) { - logger.report( - onBrokenMarkdownLinks, - )`Blog markdown link couldn't be resolved: (url=${linkPathname}) in source file path=${sourceFilePath}`; - } - return permalink; - }, - }; - return { - loader: require.resolve('@docusaurus/mdx-loader'), - options: loaderOptions, - }; - } - - function createBlogMarkdownLoader(): RuleSetUseItem { - const loaderOptions: BlogMarkdownLoaderOptions = { - truncateMarker, - }; - return { - loader: path.resolve(__dirname, './markdownLoader.js'), - options: loaderOptions, - }; - } - return { resolve: { alias: { @@ -365,15 +374,7 @@ export default async function pluginContentBlog( }, }, module: { - rules: [ - { - test: /\.mdx?$/i, - include: contentDirs - // Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970 - .map(addTrailingPathSeparator), - use: [createMDXLoader(), createBlogMarkdownLoader()], - }, - ], + rules: [blogMDXLoaderRule], }, }; }, diff --git a/packages/docusaurus-plugin-content-docs/src/index.ts b/packages/docusaurus-plugin-content-docs/src/index.ts index 878ffd34c4..5ccb28a331 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/index.ts @@ -26,6 +26,10 @@ import { getTagsFile, getTagsFilePathsToWatch, } from '@docusaurus/utils-validation'; +import { + createMDXLoaderRule, + type Options as MDXLoaderOptions, +} from '@docusaurus/mdx-loader'; import {loadSidebars, resolveSidebarPathOption} from './sidebars'; import {CategoryMetadataFilenamePattern} from './sidebars/generator'; import { @@ -49,7 +53,6 @@ import { } from './translations'; import {createAllRoutes} from './routes'; import {createSidebarsUtils} from './sidebars/utils'; -import type {Options as MDXLoaderOptions} from '@docusaurus/mdx-loader'; import type { PluginOptions, @@ -61,7 +64,7 @@ import type { } from '@docusaurus/plugin-content-docs'; import type {LoadContext, Plugin} from '@docusaurus/types'; import type {DocFile, FullVersion} from './types'; -import type {RuleSetUseItem} from 'webpack'; +import type {RuleSetRule} from 'webpack'; // TODO this is bad, we should have a better way to do this (new lifecycle?) // The source to permalink is currently a mutable map passed to the mdx loader @@ -114,6 +117,68 @@ export default async function pluginContentDocs( const sourceToPermalinkHelper = createSourceToPermalinkHelper(); + 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); + + const loaderOptions: MDXLoaderOptions = { + 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`); + }, + // Assets allow to convert some relative images paths to + // require(...) calls + createAssets: ({frontMatter}: {frontMatter: DocFrontMatter}) => ({ + image: frontMatter.image, + }), + markdownConfig: siteConfig.markdown, + resolveMarkdownLink: ({linkPathname, sourceFilePath}) => { + const version = getVersionFromSourceFilePath( + sourceFilePath, + versionsMetadata, + ); + const permalink = resolveMarkdownLinkPathname(linkPathname, { + sourceFilePath, + sourceToPermalink: sourceToPermalinkHelper.get(), + siteDir, + contentPaths: version, + }); + if (permalink === null) { + logger.report( + siteConfig.onBrokenMarkdownLinks, + )`Docs markdown link couldn't be resolved: (url=${linkPathname}) in source file path=${sourceFilePath} for version number=${version.versionName}`; + } + return permalink; + }, + }; + + return createMDXLoaderRule({include: contentDirs, options: loaderOptions}); + } + + const docsMDXLoaderRule = await createDocsMDXLoaderRule(); + return { name: 'docusaurus-plugin-content-docs', @@ -289,74 +354,7 @@ export default async function pluginContentDocs( }); }, - configureWebpack(_config, isServer, utils, content) { - 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); - - function createMDXLoader(): RuleSetUseItem { - const loaderOptions: MDXLoaderOptions = { - 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`); - }, - // Assets allow to convert some relative images paths to - // require(...) calls - createAssets: ({frontMatter}: {frontMatter: DocFrontMatter}) => ({ - image: frontMatter.image, - }), - markdownConfig: siteConfig.markdown, - resolveMarkdownLink: ({linkPathname, sourceFilePath}) => { - const version = getVersionFromSourceFilePath( - sourceFilePath, - content.loadedVersions, - ); - const permalink = resolveMarkdownLinkPathname(linkPathname, { - sourceFilePath, - sourceToPermalink: sourceToPermalinkHelper.get(), - siteDir, - contentPaths: version, - }); - if (permalink === null) { - logger.report( - siteConfig.onBrokenMarkdownLinks, - )`Docs markdown link couldn't be resolved: (url=${linkPathname}) in source file path=${sourceFilePath} for version number=${version.versionName}`; - } - return permalink; - }, - }; - - return { - loader: require.resolve('@docusaurus/mdx-loader'), - options: loaderOptions, - }; - } - + configureWebpack() { return { ignoreWarnings: [ // Suppress warnings about non-existing of versions file. @@ -370,13 +368,7 @@ export default async function pluginContentDocs( }, }, module: { - rules: [ - { - test: /\.mdx?$/i, - include: contentDirs, - use: [createMDXLoader()], - }, - ], + rules: [docsMDXLoaderRule], }, }; }, diff --git a/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.ts index 24870c77bc..b1f14ebcbb 100644 --- a/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.ts @@ -16,7 +16,7 @@ describe('docusaurus-plugin-content-pages', () => { it('loads simple pages', async () => { const siteDir = path.join(__dirname, '__fixtures__', 'website'); const context = await loadContext({siteDir}); - const plugin = pluginContentPages( + const plugin = await pluginContentPages( context, validateOptions({ validate: normalizePluginOptions, @@ -33,7 +33,7 @@ describe('docusaurus-plugin-content-pages', () => { it('loads simple pages with french translations', async () => { const siteDir = path.join(__dirname, '__fixtures__', 'website'); const context = await loadContext({siteDir, locale: 'fr'}); - const plugin = pluginContentPages( + const plugin = await pluginContentPages( context, validateOptions({ validate: normalizePluginOptions, @@ -50,7 +50,7 @@ describe('docusaurus-plugin-content-pages', () => { it('loads simple pages with last update', async () => { const siteDir = path.join(__dirname, '__fixtures__', 'website'); const context = await loadContext({siteDir}); - const plugin = pluginContentPages( + const plugin = await pluginContentPages( context, validateOptions({ validate: normalizePluginOptions, diff --git a/packages/docusaurus-plugin-content-pages/src/index.ts b/packages/docusaurus-plugin-content-pages/src/index.ts index d974010a3a..82c2a975ce 100644 --- a/packages/docusaurus-plugin-content-pages/src/index.ts +++ b/packages/docusaurus-plugin-content-pages/src/index.ts @@ -14,6 +14,10 @@ import { createAbsoluteFilePathMatcher, DEFAULT_PLUGIN_ID, } from '@docusaurus/utils'; +import { + createMDXLoaderRule, + type Options as MDXLoaderOptions, +} from '@docusaurus/mdx-loader'; import {createAllRoutes} from './routes'; import { createPagesContentPaths, @@ -26,13 +30,12 @@ import type { LoadedContent, PageFrontMatter, } from '@docusaurus/plugin-content-pages'; -import type {RuleSetUseItem} from 'webpack'; -import type {Options as MDXLoaderOptions} from '@docusaurus/mdx-loader/lib/loader'; +import type {RuleSetRule} from 'webpack'; -export default function pluginContentPages( +export default async function pluginContentPages( context: LoadContext, options: PluginOptions, -): Plugin { +): Promise> { const {siteConfig, siteDir, generatedFilesDir} = context; const contentPaths = createPagesContentPaths({context, options}); @@ -43,6 +46,53 @@ export default function pluginContentPages( ); const dataDir = path.join(pluginDataDirRoot, options.id ?? DEFAULT_PLUGIN_ID); + async function createPagesMDXLoaderRule(): Promise { + const { + admonitions, + rehypePlugins, + remarkPlugins, + recmaPlugins, + beforeDefaultRehypePlugins, + beforeDefaultRemarkPlugins, + } = options; + const contentDirs = getContentPathList(contentPaths); + + const loaderOptions: MDXLoaderOptions = { + 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 aliasedSource = aliasedSitePath(mdxPath, siteDir); + return path.join(dataDir, `${docuHash(aliasedSource)}.json`); + }, + // Assets allow to convert some relative images paths to + // require(...) calls + createAssets: ({frontMatter}: {frontMatter: PageFrontMatter}) => ({ + image: frontMatter.image, + }), + markdownConfig: siteConfig.markdown, + }; + + return createMDXLoaderRule({ + include: contentDirs + // Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970 + .map(addTrailingPathSeparator), + options: loaderOptions, + }); + } + + const pagesMDXLoaderRule = await createPagesMDXLoaderRule(); + return { name: 'docusaurus-plugin-content-pages', @@ -68,63 +118,9 @@ export default function pluginContentPages( }, configureWebpack() { - const { - admonitions, - rehypePlugins, - remarkPlugins, - recmaPlugins, - beforeDefaultRehypePlugins, - beforeDefaultRemarkPlugins, - } = options; - const contentDirs = getContentPathList(contentPaths); - - function createMDXLoader(): RuleSetUseItem { - const loaderOptions: MDXLoaderOptions = { - 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 aliasedSource = aliasedSitePath(mdxPath, siteDir); - return path.join(dataDir, `${docuHash(aliasedSource)}.json`); - }, - // Assets allow to convert some relative images paths to - // require(...) calls - createAssets: ({frontMatter}: {frontMatter: PageFrontMatter}) => ({ - image: frontMatter.image, - }), - markdownConfig: siteConfig.markdown, - }; - - return { - loader: require.resolve('@docusaurus/mdx-loader'), - options: loaderOptions, - }; - } - return { module: { - rules: [ - { - test: /\.mdx?$/i, - include: contentDirs - // Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970 - .map(addTrailingPathSeparator), - use: [createMDXLoader()], - }, - ], + rules: [pagesMDXLoaderRule], }, }; }, diff --git a/packages/docusaurus/src/server/plugins/__tests__/plugins.test.ts b/packages/docusaurus/src/server/plugins/__tests__/plugins.test.ts index 1ec86de26f..9e753643e4 100644 --- a/packages/docusaurus/src/server/plugins/__tests__/plugins.test.ts +++ b/packages/docusaurus/src/server/plugins/__tests__/plugins.test.ts @@ -28,6 +28,7 @@ async function testLoad({ baseUrl: '/', trailingSlash: true, themeConfig: {}, + staticDirectories: [], presets: [], plugins, themes, diff --git a/packages/docusaurus/src/server/plugins/plugins.ts b/packages/docusaurus/src/server/plugins/plugins.ts index 631e586644..59aed351ac 100644 --- a/packages/docusaurus/src/server/plugins/plugins.ts +++ b/packages/docusaurus/src/server/plugins/plugins.ts @@ -266,7 +266,7 @@ export async function loadPlugins( // TODO probably not the ideal place to hardcode those plugins initializedPlugins.push( createBootstrapPlugin(context), - createMDXFallbackPlugin(context), + await createMDXFallbackPlugin(context), ); const plugins = await executeAllPluginsContentLoading({ diff --git a/packages/docusaurus/src/server/plugins/synthetic.ts b/packages/docusaurus/src/server/plugins/synthetic.ts index ba68959963..500c824921 100644 --- a/packages/docusaurus/src/server/plugins/synthetic.ts +++ b/packages/docusaurus/src/server/plugins/synthetic.ts @@ -6,13 +6,13 @@ */ import path from 'path'; +import {createMDXLoaderItem} from '@docusaurus/mdx-loader'; import type {RuleSetRule} from 'webpack'; import type { HtmlTagObject, LoadContext, InitializedPlugin, } from '@docusaurus/types'; -import type {Options as MDXLoaderOptions} from '@docusaurus/mdx-loader'; /** * Make a synthetic plugin to: @@ -75,10 +75,23 @@ export function createBootstrapPlugin({ * content plugins. This allows to do things such as importing repo/README.md as * a partial from another doc. Not ideal solution, but good enough for now */ -export function createMDXFallbackPlugin({ +export async function createMDXFallbackPlugin({ siteDir, siteConfig, -}: LoadContext): InitializedPlugin { +}: LoadContext): Promise { + const mdxLoaderItem = await createMDXLoaderItem({ + admonitions: true, + staticDirs: siteConfig.staticDirectories.map((dir) => + path.resolve(siteDir, dir), + ), + siteDir, + // External MDX files are always meant to be imported as partials + isMDXPartial: () => true, + // External MDX files might have front matter, just disable the warning + isMDXPartialFrontMatterWarningDisabled: true, + markdownConfig: siteConfig.markdown, + }); + return { name: 'docusaurus-mdx-fallback-plugin', options: { @@ -99,18 +112,6 @@ export function createMDXFallbackPlugin({ return isMDXRule ? (rule.include as string[]) : []; }); } - const mdxLoaderOptions: MDXLoaderOptions = { - admonitions: true, - staticDirs: siteConfig.staticDirectories.map((dir) => - path.resolve(siteDir, dir), - ), - siteDir, - // External MDX files are always meant to be imported as partials - isMDXPartial: () => true, - // External MDX files might have front matter, just disable the warning - isMDXPartialFrontMatterWarningDisabled: true, - markdownConfig: siteConfig.markdown, - }; return { module: { @@ -118,12 +119,7 @@ export function createMDXFallbackPlugin({ { test: /\.mdx?$/i, exclude: getMDXFallbackExcludedPaths(), - use: [ - { - loader: require.resolve('@docusaurus/mdx-loader'), - options: mdxLoaderOptions, - }, - ], + use: [mdxLoaderItem], }, ], },