mirror of
https://github.com/facebook/docusaurus.git
synced 2025-07-23 19:48:54 +02:00
refactor(mdx-loader): refactor mdx-loader, expose loader creation utils (#10450)
This commit is contained in:
parent
db6c2af160
commit
d5885c0c5d
13 changed files with 494 additions and 413 deletions
48
packages/docusaurus-mdx-loader/src/createMDXLoader.ts
Normal file
48
packages/docusaurus-mdx-loader/src/createMDXLoader.ts
Normal file
|
@ -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<Options> {
|
||||
// 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<RuleSetUseItem> {
|
||||
return {
|
||||
loader: require.resolve('@docusaurus/mdx-loader'),
|
||||
options: await enhancedOptions(options),
|
||||
};
|
||||
}
|
||||
|
||||
export async function createMDXLoaderRule({
|
||||
include,
|
||||
options,
|
||||
}: {
|
||||
include: RuleSetRule['include'];
|
||||
options: Options;
|
||||
}): Promise<RuleSetRule> {
|
||||
return {
|
||||
test: /\.mdx?$/i,
|
||||
include,
|
||||
use: [await createMDXLoaderItem(options)],
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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<MDXOptions> & {
|
|||
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<Options>,
|
||||
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);
|
||||
|
|
|
@ -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<string | Options, ProcessorsCacheEntry>();
|
||||
const ProcessorsCache = new Map<string | Options, SimpleProcessors>();
|
||||
|
||||
async function createProcessorsCacheEntry({
|
||||
export async function createProcessors({
|
||||
options,
|
||||
}: {
|
||||
options: Options;
|
||||
}): Promise<ProcessorsCacheEntry> {
|
||||
}): Promise<SimpleProcessors> {
|
||||
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<SimpleProcessors> {
|
||||
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<SimpleProcessor> {
|
||||
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;
|
||||
}
|
||||
|
|
152
packages/docusaurus-mdx-loader/src/utils.ts
Normal file
152
packages/docusaurus-mdx-loader/src/utils.ts
Normal file
|
@ -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<string> {
|
||||
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<string, unknown>;
|
||||
options: Options;
|
||||
compilerName: WebpackCompilerName;
|
||||
}): Promise<SimpleProcessorResult> {
|
||||
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},
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue