Skip to content

Commit

Permalink
MDX: Support reusing content with include tag
Browse files Browse the repository at this point in the history
  • Loading branch information
fuma-nama committed Dec 20, 2024
1 parent cc80f5b commit bd0a140
Show file tree
Hide file tree
Showing 9 changed files with 115 additions and 57 deletions.
5 changes: 5 additions & 0 deletions .changeset/cuddly-lamps-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'fumadocs-mdx': minor
---

Support reusing content with `include` tag
2 changes: 2 additions & 0 deletions examples/next-mdx/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ Hello World dsasfdafsd
| very **important** | Hey |
| _Surprisingly_ | Fumadocs |
| very long text that looks weird | hello world hello world hello world |

<include>./test.mdx</include>
2 changes: 2 additions & 0 deletions packages/mdx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"fast-glob": "^3.3.1",
"gray-matter": "^4.0.3",
"micromatch": "^4.0.8",
"unist-util-visit": "^5.0.0",
"zod": "^3.24.1"
},
"devDependencies": {
Expand All @@ -60,6 +61,7 @@
"@types/react": "^19.0.1",
"eslint-config-custom": "workspace:*",
"fumadocs-core": "workspace:*",
"mdast-util-mdx-jsx": "^3.1.3",
"next": "^15.1.1",
"tsconfig": "workspace:*",
"unified": "^11.0.5",
Expand Down
49 changes: 47 additions & 2 deletions packages/mdx/src/config/load.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import * as path from 'node:path';
import { pathToFileURL } from 'node:url';
import { build } from 'esbuild';
import { validateConfig } from '@/config/validate';
import type { DocCollection, MetaCollection } from '@/config/define';
import { type GlobalConfig } from '@/config/types';
import type { ProcessorOptions } from '@mdx-js/mdx';
import { getDefaultMDXOptions } from '@/utils/mdx-options';

export function findConfigFile(): string {
return path.resolve('source.config.ts');
}

export interface LoadedConfig {
collections: Map<string, InternalDocCollection | InternalMetaCollection>;
defaultMdxOptions: ProcessorOptions;
global?: GlobalConfig;

_runtime: {
Expand Down Expand Up @@ -48,7 +50,7 @@ export async function loadConfig(configPath: string): Promise<LoadedConfig> {
}

const url = pathToFileURL(outputPath);
const [err, config] = validateConfig(
const [err, config] = buildConfig(
// every call to `loadConfig` will cause the previous cache to be ignored
(await import(`${url.toString()}?hash=${Date.now().toString()}`)) as Record<
string,
Expand All @@ -59,3 +61,46 @@ export async function loadConfig(configPath: string): Promise<LoadedConfig> {
if (err !== null) throw new Error(err);
return config;
}

function buildConfig(
config: Record<string, unknown>,
): [err: string, value: null] | [err: null, value: LoadedConfig] {
const collections: LoadedConfig['collections'] = new Map();
let globalConfig: LoadedConfig['global'];

for (const [k, v] of Object.entries(config)) {
if (!v) {
continue;
}

if (typeof v === 'object' && '_doc' in v && v._doc === 'collections') {
collections.set(
k,
v as unknown as InternalMetaCollection | InternalDocCollection,
);
continue;
}

if (k === 'default') {
globalConfig = v as GlobalConfig;
continue;
}

return [
`Unknown export "${k}", you can only export collections from source configuration file.`,
null,
];
}

return [
null,
{
global: globalConfig,
collections,
defaultMdxOptions: getDefaultMDXOptions(globalConfig?.mdxOptions ?? {}),
_runtime: {
files: new Map(),
},
},
];
}
43 changes: 0 additions & 43 deletions packages/mdx/src/config/validate.ts

This file was deleted.

9 changes: 4 additions & 5 deletions packages/mdx/src/loader-mdx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { type LoaderContext } from 'webpack';
import { type StructuredData } from 'fumadocs-core/mdx-plugins';
import { getConfigHash, loadConfigCached } from '@/config/cached';
import { buildMDX } from '@/utils/build-mdx';
import { getDefaultMDXOptions, type TransformContext } from '@/config';
import { type TransformContext } from '@/config';
import { getManifestEntryPath } from '@/map/manifest';
import { formatError } from '@/utils/format-error';
import { getGitTimestamp } from './utils/git-timestamp';
Expand Down Expand Up @@ -85,13 +85,11 @@ export default async function loader(
collection = undefined;
}

const mdxOptions =
collection?.mdxOptions ??
getDefaultMDXOptions(config.global?.mdxOptions ?? {});
const mdxOptions = collection?.mdxOptions ?? config.defaultMdxOptions;

function getTransformContext(): TransformContext {
return {
buildMDX: async (v, options = mdxOptions) => {
async buildMDX(v, options = mdxOptions) {
const res = await buildMDX(
collectionId ?? 'global',
configHash,
Expand Down Expand Up @@ -148,6 +146,7 @@ export default async function loader(
data: {
lastModified: timestamp,
},
_compiler: this,
},
);

Expand Down
7 changes: 4 additions & 3 deletions packages/mdx/src/map/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export async function generateJS(
configPath: string,
config: LoadedConfig,
outputPath: string,
hash: string,
configHash: string,
getFrontmatter: (file: string) => Promise<unknown>,
): Promise<string> {
const outDir = path.dirname(outputPath);
Expand All @@ -33,7 +33,7 @@ export async function generateJS(
config._runtime.files.set(file.absolutePath, k);

if (collection.type === 'doc' && collection.async) {
const importPath = `${toImportPath(file.absolutePath, outDir)}?hash=${hash}&collection=${k}`;
const importPath = `${toImportPath(file.absolutePath, outDir)}?hash=${configHash}&collection=${k}`;
const frontmatter = await getFrontmatter(file.absolutePath);

return `toRuntimeAsync(${JSON.stringify(frontmatter)}, () => import(${JSON.stringify(importPath)}), ${JSON.stringify(file)})`;
Expand All @@ -43,7 +43,7 @@ export async function generateJS(
imports.push({
type: 'namespace',
name: importName,
specifier: `${toImportPath(file.absolutePath, outDir)}?collection=${k}&hash=${hash}`,
specifier: `${toImportPath(file.absolutePath, outDir)}?collection=${k}&hash=${configHash}`,
});

return `toRuntime("${collection.type}", ${importName}, ${JSON.stringify(file)})`;
Expand All @@ -55,6 +55,7 @@ export async function generateJS(
importedCollections.add(k);
}

// TODO: remove `transform` API on next major, migrate to runtime transform (e.g. transform & re-export in `source.ts`)
return collection.transform
? `export const ${k} = await Promise.all([${resolvedItems.join(', ')}].map(v => c_${k}.transform(v, ${config.global ? 'c_default' : 'undefined'})));`
: `export const ${k} = [${resolvedItems.join(', ')}];`;
Expand Down
49 changes: 45 additions & 4 deletions packages/mdx/src/utils/build-mdx.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { createProcessor, type ProcessorOptions } from '@mdx-js/mdx';
import type { VFile } from 'vfile';
import type { Transformer } from 'unified';
import { visit } from 'unist-util-visit';
import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx';
import type { Literal } from 'mdast';
import { readFileSync } from 'node:fs';
import * as path from 'node:path';
import matter from 'gray-matter';

const cache = new Map<
string,
{ processor: ReturnType<typeof createProcessor>; configHash: string }
>();
type Processor = ReturnType<typeof createProcessor>;

const cache = new Map<string, { processor: Processor; configHash: string }>();

export interface MDXOptions extends ProcessorOptions {
/**
Expand All @@ -23,12 +29,41 @@ export interface MDXOptions extends ProcessorOptions {
* Custom Vfile data
*/
data?: Record<string, unknown>;

_compiler?: CompilerOptions;
}

interface CompilerOptions {
addDependency: (file: string) => void;
}

function cacheKey(group: string, format: string): string {
return `${group}:${format}`;
}

function remarkInclude(this: Processor, options: CompilerOptions): Transformer {
return (tree, file) => {
visit(tree, 'mdxJsxFlowElement', (node: MdxJsxFlowElement) => {
if (node.name === 'include') {
const child = node.children.at(0) as Literal | undefined;

if (!child || child.type !== 'text') return;
const specifier = child.value;

const targetPath = path.resolve(path.dirname(file.path), specifier);

const content = readFileSync(targetPath);
const parsed = this.parse(matter(content).content);

options.addDependency(targetPath);
Object.assign(node, parsed);
}

return 'skip';
});
};
}

/**
* @param group - The cache group of MDX content, usually the collection name
* @param configHash - config hash
Expand Down Expand Up @@ -58,6 +93,12 @@ export function buildMDX(
outputFormat: 'program',
development: process.env.NODE_ENV === 'development',
...rest,
remarkPlugins: [
options._compiler
? ([remarkInclude, options._compiler] as any)
: null,
...(rest.remarkPlugins ?? []),
],
format,
}),

Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit bd0a140

Please sign in to comment.