Skip to content

Commit

Permalink
refactor(types): move non-core, non-public types out of the types pac…
Browse files Browse the repository at this point in the history
…kage
  • Loading branch information
Josh-Cena committed May 3, 2022
1 parent c7a5af7 commit 4b32dd2
Show file tree
Hide file tree
Showing 25 changed files with 397 additions and 404 deletions.
2 changes: 1 addition & 1 deletion packages/docusaurus-mdx-loader/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "2.0.0-beta.18",
"description": "Docusaurus Loader for MDX",
"main": "lib/index.js",
"types": "src/mdx-loader.d.ts",
"types": "lib/index.d.ts",
"publishConfig": {
"access": "public"
},
Expand Down
9 changes: 6 additions & 3 deletions packages/docusaurus-mdx-loader/src/deps.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@

// TODO Types provided by MDX 2.0 https://github.com/mdx-js/mdx/blob/main/packages/mdx/types/index.d.ts
declare module '@mdx-js/mdx' {
import type {Processor} from 'unified';
import type {MDXPlugin} from '@docusaurus/mdx-loader';
import type {Processor, Plugin} from 'unified';

export type Options = {
type MDXPlugin =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[Plugin<any[]>, any] | Plugin<any[]>;

type Options = {
filepath?: string;
skipExport?: boolean;
wrapExport?: string;
Expand Down
255 changes: 23 additions & 232 deletions packages/docusaurus-mdx-loader/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,240 +5,31 @@
* LICENSE file in the root directory of this source tree.
*/

import fs from 'fs-extra';
import {createCompiler} from '@mdx-js/mdx';
import logger from '@docusaurus/logger';
import emoji from 'remark-emoji';
import {
parseFrontMatter,
parseMarkdownContentTitle,
escapePath,
getFileLoaderUtils,
} from '@docusaurus/utils';
import stringifyObject from 'stringify-object';
import headings from './remark/headings';
import toc from './remark/toc';
import unwrapMdxCodeBlocks from './remark/unwrapMdxCodeBlocks';
import transformImage from './remark/transformImage';
import transformLinks from './remark/transformLinks';
import type {MDXOptions} from '@docusaurus/mdx-loader';
import type {LoaderContext} from 'webpack';
import type {Processor} from 'unified';
import {mdxLoader} from './loader';

const {
loaders: {inlineMarkdownImageFileLoader},
} = getFileLoaderUtils();
export default mdxLoader;

const pragma = `
/* @jsxRuntime classic */
/* @jsx mdx */
/* @jsxFrag mdx.Fragment */
`;

const DEFAULT_OPTIONS: MDXOptions = {
rehypePlugins: [],
remarkPlugins: [unwrapMdxCodeBlocks, emoji, headings, toc],
beforeDefaultRemarkPlugins: [],
beforeDefaultRehypePlugins: [],
export type TOCItem = {
readonly value: string;
readonly id: string;
readonly level: number;
};

const compilerCache = new Map<string | Options, [Processor, Options]>();

type Options = MDXOptions & {
staticDirs: string[];
siteDir: string;
isMDXPartial?: (filePath: string) => boolean;
isMDXPartialFrontMatterWarningDisabled?: boolean;
removeContentTitle?: boolean;
metadataPath?: string | ((filePath: string) => string);
createAssets?: (metadata: {
frontMatter: {[key: string]: unknown};
metadata: {[key: string]: unknown};
}) => {[key: string]: unknown};
filepath: string;
export type LoadedMDXContent<FrontMatter, Metadata, Assets = undefined> = {
/** As verbatim declared in the MDX document. */
readonly frontMatter: FrontMatter;
/** As provided by the content plugin. */
readonly metadata: Metadata;
/** A list of TOC items (headings). */
readonly toc: readonly TOCItem[];
/** First h1 title before any content. */
readonly contentTitle: string | undefined;
/**
* Usually image assets that may be collocated like `./img/thumbnail.png`.
* The loader would also bundle these assets and the client should use these
* in priority.
*/
readonly assets: Assets;
(): JSX.Element;
};

/**
* 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: {[key: string]: unknown}) {
if (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) => 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)
const inlineLoader = inlineMarkdownImageFileLoader;
return `require("${inlineLoader}${escapePath(assetValue)}").default`;
}
return undefined;
}

const assetEntries = Object.entries(assets);

const codeLines = assetEntries
.map(([key, value]) => {
const assetRequireCode = createAssetValueCode(value);
return assetRequireCode ? `"${key}": ${assetRequireCode},` : undefined;
})
.filter(Boolean);

return `{\n${codeLines.join('\n')}\n}`;
}

export default async function mdxLoader(
this: LoaderContext<Options>,
fileString: string,
): Promise<void> {
const callback = this.async();
const filePath = this.resourcePath;
const reqOptions = this.getOptions() ?? {};

const {frontMatter, content: contentWithTitle} = parseFrontMatter(fileString);

const {content, contentTitle} = parseMarkdownContentTitle(contentWithTitle, {
removeContentTitle: reqOptions.removeContentTitle,
});

const hasFrontMatter = Object.keys(frontMatter).length > 0;

if (!compilerCache.has(this.query)) {
const options: Options = {
...reqOptions,
remarkPlugins: [
...(reqOptions.beforeDefaultRemarkPlugins ?? []),
...DEFAULT_OPTIONS.remarkPlugins,
[
transformImage,
{
staticDirs: reqOptions.staticDirs,
siteDir: reqOptions.siteDir,
},
],
[
transformLinks,
{
staticDirs: reqOptions.staticDirs,
siteDir: reqOptions.siteDir,
},
],
...(reqOptions.remarkPlugins ?? []),
],
rehypePlugins: [
...(reqOptions.beforeDefaultRehypePlugins ?? []),
...DEFAULT_OPTIONS.rehypePlugins,
...(reqOptions.rehypePlugins ?? []),
],
};
compilerCache.set(this.query, [createCompiler(options), options]);
}

const [compiler, options] = compilerCache.get(this.query)!;

let result: string;
try {
result = await compiler
.process({
contents: content,
path: this.resourcePath,
})
.then((res) => res.toString());
} catch (err) {
return callback(err as Error);
}

// MDX partials are MDX files starting with _ or in a folder starting with _
// Partial are not expected to have associated metadata files or front matter
const isMDXPartial = options.isMDXPartial?.(filePath);
if (isMDXPartial && hasFrontMatter) {
const errorMessage = `Docusaurus MDX partial files should not contain front matter.
Those partial files use the _ prefix as a convention by default, but this is configurable.
File at ${filePath} contains front matter that will be ignored:
${JSON.stringify(frontMatter, null, 2)}`;

if (!options.isMDXPartialFrontMatterWarningDisabled) {
const shouldError = process.env.NODE_ENV === 'test' || process.env.CI;
if (shouldError) {
return callback(new Error(errorMessage));
}
logger.warn(errorMessage);
}
}

function getMetadataPath(): string | undefined {
if (!isMDXPartial) {
// Read metadata for this MDX and export it.
if (options.metadataPath && typeof options.metadataPath === 'function') {
return options.metadataPath(filePath);
}
}
return undefined;
}

const metadataPath = getMetadataPath();
if (metadataPath) {
this.addDependency(metadataPath);
}

const metadataJsonString = metadataPath
? await readMetadataPath(metadataPath)
: undefined;

const metadata = metadataJsonString
? JSON.parse(metadataJsonString)
: undefined;

const assets =
reqOptions.createAssets && metadata
? reqOptions.createAssets({frontMatter, metadata})
: undefined;

const exportsCode = `
export const frontMatter = ${stringifyObject(frontMatter)};
export const contentTitle = ${stringifyObject(contentTitle)};
${metadataJsonString ? `export const metadata = ${metadataJsonString};` : ''}
${assets ? `export const assets = ${createAssetsExportCode(assets)};` : ''}
`;

const code = `
${pragma}
import React from 'react';
import { mdx } from '@mdx-js/react';
${exportsCode}
${result}
`;

return callback(null, code);
}
export type {Options, MDXPlugin, MDXOptions} from './loader';
Loading

0 comments on commit 4b32dd2

Please sign in to comment.