mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 23:49:51 +00:00
MDX: Support getText() API
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-mdx': minor
|
||||
---
|
||||
|
||||
Unify doc collection entry for both Vite and Next.js integrations
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-mdx': minor
|
||||
---
|
||||
|
||||
Support `getText()` API & `postprocess.includeProcessedMarkdown` option, deprecate `content` in favour of `getText()`.
|
||||
@@ -77,6 +77,7 @@
|
||||
"estree-util-value-to-estree": "^3.4.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lru-cache": "^11.2.1",
|
||||
"mdast-util-to-markdown": "^2.1.2",
|
||||
"picocolors": "^1.1.1",
|
||||
"remark-mdx": "^3.1.1",
|
||||
"remark-parse": "^11.0.0",
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { StandardSchemaV1 } from '@standard-schema/spec';
|
||||
import type { DefaultMDXOptions } from '@/utils/mdx-options';
|
||||
import type { ProcessorOptions } from '@mdx-js/mdx';
|
||||
import { frontmatterSchema, metaSchema } from '@/config/zod-4';
|
||||
import type { PostprocessOptions } from '@/mdx-plugins/remark-postprocess';
|
||||
|
||||
export type CollectionSchema<Schema extends StandardSchemaV1, Context> =
|
||||
| Schema
|
||||
@@ -37,6 +38,7 @@ export interface DocCollection<
|
||||
> extends BaseCollection {
|
||||
type: 'doc';
|
||||
|
||||
postprocess?: PostprocessOptions;
|
||||
mdxOptions?: ProcessorOptions;
|
||||
|
||||
/**
|
||||
|
||||
@@ -98,7 +98,7 @@ export function toWebpack(loader: Loader): WebpackLoader {
|
||||
try {
|
||||
const result = await loader({
|
||||
filePath: this.resourcePath,
|
||||
query: parse(this.resourceQuery),
|
||||
query: parse(this.resourceQuery.slice(1)),
|
||||
source,
|
||||
development: this.mode === 'development',
|
||||
compiler: this,
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { ConfigLoader } from '@/loaders/config-loader';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { createHash } from 'node:crypto';
|
||||
import type { DocCollection } from '@/config';
|
||||
|
||||
const querySchema = z
|
||||
.object({
|
||||
@@ -62,22 +63,19 @@ export function createMdxLoader(configLoader: ConfigLoader): Loader {
|
||||
? loaded.collections.get(parsed.collection)
|
||||
: undefined;
|
||||
|
||||
let schema;
|
||||
let mdxOptions;
|
||||
let docCollection: DocCollection | undefined;
|
||||
switch (collection?.type) {
|
||||
case 'doc':
|
||||
mdxOptions = collection.mdxOptions;
|
||||
schema = collection.schema;
|
||||
docCollection = collection;
|
||||
break;
|
||||
case 'docs':
|
||||
mdxOptions = collection.docs.mdxOptions;
|
||||
schema = collection.docs.schema;
|
||||
docCollection = collection.docs;
|
||||
break;
|
||||
}
|
||||
|
||||
if (schema) {
|
||||
if (docCollection?.schema) {
|
||||
matter.data = await validate(
|
||||
schema,
|
||||
docCollection.schema,
|
||||
matter.data,
|
||||
{
|
||||
source: value,
|
||||
@@ -107,7 +105,8 @@ export function createMdxLoader(configLoader: ConfigLoader): Loader {
|
||||
'\n'.repeat(lineOffset) + matter.content,
|
||||
{
|
||||
development: isDevelopment,
|
||||
...(mdxOptions ?? (await loaded.getDefaultMDXOptions())),
|
||||
...(docCollection?.mdxOptions ?? (await loaded.getDefaultMDXOptions())),
|
||||
postprocess: docCollection?.postprocess,
|
||||
data,
|
||||
filePath,
|
||||
frontmatter: matter.data as Record<string, unknown>,
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { Processor, Transformer } from 'unified';
|
||||
import type { Root, RootContent } from 'mdast';
|
||||
import { valueToEstree } from 'estree-util-value-to-estree';
|
||||
|
||||
export interface RemarkMdxExportOptions {
|
||||
/**
|
||||
* Values to export from `vfile.data`
|
||||
*/
|
||||
dataNames?: string[];
|
||||
}
|
||||
|
||||
declare module 'vfile' {
|
||||
interface DataMap {
|
||||
/**
|
||||
* [Fumadocs MDX] additional ESM exports to write
|
||||
*/
|
||||
'mdx-export'?: { name: string; value: unknown }[];
|
||||
}
|
||||
}
|
||||
|
||||
export function remarkMdxExport(
|
||||
this: Processor,
|
||||
{ dataNames = [] }: RemarkMdxExportOptions = {},
|
||||
): Transformer<Root, Root> {
|
||||
return (tree, file) => {
|
||||
for (const { name, value } of file.data['mdx-export'] ?? []) {
|
||||
tree.children.unshift(getMdastExport(name, value));
|
||||
}
|
||||
|
||||
for (const name of dataNames) {
|
||||
if (!(name in file.data)) continue;
|
||||
|
||||
tree.children.unshift(getMdastExport(name, file.data[name]));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* MDX.js first converts javascript (with esm support) into mdast nodes with remark-mdx, then handle the other remark plugins
|
||||
*
|
||||
* Therefore, if we want to inject an export, we must convert the object into AST, then add the mdast node
|
||||
*/
|
||||
function getMdastExport(name: string, value: unknown): RootContent {
|
||||
return {
|
||||
type: 'mdxjsEsm',
|
||||
value: '',
|
||||
data: {
|
||||
estree: {
|
||||
type: 'Program',
|
||||
sourceType: 'module',
|
||||
body: [
|
||||
{
|
||||
type: 'ExportNamedDeclaration',
|
||||
attributes: [],
|
||||
specifiers: [],
|
||||
source: null,
|
||||
declaration: {
|
||||
type: 'VariableDeclaration',
|
||||
kind: 'let',
|
||||
declarations: [
|
||||
{
|
||||
type: 'VariableDeclarator',
|
||||
id: {
|
||||
type: 'Identifier',
|
||||
name,
|
||||
},
|
||||
init: valueToEstree(value),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
import type { Transformer } from 'unified';
|
||||
import type { Processor, Transformer } from 'unified';
|
||||
import type { Root, RootContent } from 'mdast';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import { valueToEstree } from 'estree-util-value-to-estree';
|
||||
import { toMarkdown } from 'mdast-util-to-markdown';
|
||||
|
||||
declare module 'vfile' {
|
||||
interface DataMap {
|
||||
extractedReferences: ExtractedReference[];
|
||||
|
||||
/**
|
||||
* [Fumadocs MDX] Processed Markdown content before `remark-rehype`.
|
||||
*/
|
||||
_markdown?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,19 +20,19 @@ export interface ExtractedReference {
|
||||
|
||||
export interface PostprocessOptions {
|
||||
/**
|
||||
* Values to export from `vfile.data`
|
||||
* stringify MDAST and export via `_markdown`.
|
||||
*/
|
||||
injectExports: string[];
|
||||
includeProcessedMarkdown?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* - write exports
|
||||
* - collect references
|
||||
* - write frontmatter (auto-title & description)
|
||||
*/
|
||||
export function remarkPostprocess({
|
||||
injectExports,
|
||||
}: PostprocessOptions): Transformer<Root, Root> {
|
||||
export function remarkPostprocess(
|
||||
this: Processor,
|
||||
{ includeProcessedMarkdown = false }: PostprocessOptions = {},
|
||||
): Transformer<Root, Root> {
|
||||
return (tree, file) => {
|
||||
let title: string | undefined;
|
||||
const urls: ExtractedReference[] = [];
|
||||
@@ -53,11 +58,12 @@ export function remarkPostprocess({
|
||||
}
|
||||
|
||||
file.data.extractedReferences = urls;
|
||||
|
||||
for (const name of injectExports) {
|
||||
if (!(name in file.data)) continue;
|
||||
|
||||
tree.children.unshift(getMdastExport(name, file.data[name]));
|
||||
if (includeProcessedMarkdown) {
|
||||
file.data._markdown = toMarkdown(tree, {
|
||||
...this.data('settings'),
|
||||
// @ts-expect-error - from https://github.com/remarkjs/remark/blob/main/packages/remark-stringify/lib/index.js
|
||||
extensions: this.data('toMarkdownExtensions') || [],
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -70,43 +76,3 @@ function flattenNode(node: RootContent): string {
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* MDX.js first converts javascript (with esm support) into mdast nodes with remark-mdx, then handle the other remark plugins
|
||||
*
|
||||
* Therefore, if we want to inject an export, we must convert the object into AST, then add the mdast node
|
||||
*/
|
||||
function getMdastExport(name: string, value: unknown): RootContent {
|
||||
return {
|
||||
type: 'mdxjsEsm',
|
||||
value: '',
|
||||
data: {
|
||||
estree: {
|
||||
type: 'Program',
|
||||
sourceType: 'module',
|
||||
body: [
|
||||
{
|
||||
type: 'ExportNamedDeclaration',
|
||||
attributes: [],
|
||||
specifiers: [],
|
||||
source: null,
|
||||
declaration: {
|
||||
type: 'VariableDeclaration',
|
||||
kind: 'let',
|
||||
declarations: [
|
||||
{
|
||||
type: 'VariableDeclarator',
|
||||
id: {
|
||||
type: 'Identifier',
|
||||
name,
|
||||
},
|
||||
init: valueToEstree(value),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,57 +1,84 @@
|
||||
import type { LoadedConfig } from '@/utils/config';
|
||||
import { type StructuredData } from 'fumadocs-core/mdx-plugins';
|
||||
import { _runtime, createMDXSource } from './index';
|
||||
import type { RuntimeAsync } from './types';
|
||||
import { buildMDX } from '@/utils/build-mdx';
|
||||
import type { AsyncDocOut, RuntimeAsync } from './types';
|
||||
import { buildMDX, CompiledMDXProperties } from '@/utils/build-mdx';
|
||||
import type { ProcessorOptions } from '@mdx-js/mdx';
|
||||
import { executeMdx } from '@fumadocs/mdx-remote/client';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { type DocData, missingProcessedMarkdown } from '@/runtime/shared';
|
||||
import type { DocCollection } from '@/config';
|
||||
|
||||
function getDocCollection(config: LoadedConfig, collection: string) {
|
||||
const col = config.collections.get(collection);
|
||||
if (col?.type === 'doc' && col.mdxOptions) return col;
|
||||
if (col?.type === 'docs' && col.docs.mdxOptions) return col.docs;
|
||||
}
|
||||
|
||||
async function getOptions(
|
||||
config: LoadedConfig,
|
||||
collection: string,
|
||||
collection?: DocCollection,
|
||||
): Promise<ProcessorOptions> {
|
||||
const col = config.collections.get(collection);
|
||||
if (col?.type === 'doc' && col.mdxOptions) return col.mdxOptions;
|
||||
if (col?.type === 'docs' && col.docs.mdxOptions) return col.docs.mdxOptions;
|
||||
|
||||
return config.getDefaultMDXOptions('remote');
|
||||
return (
|
||||
collection?.mdxOptions ?? (await config.getDefaultMDXOptions('remote'))
|
||||
);
|
||||
}
|
||||
|
||||
export const _runtimeAsync: RuntimeAsync = {
|
||||
doc(files, collection, config) {
|
||||
doc(files, collectionName, config) {
|
||||
const collection = getDocCollection(config, collectionName);
|
||||
const initMdxOptions = getOptions(config, collection);
|
||||
|
||||
return files.map(({ info: file, data, content, lastModified }) => {
|
||||
let cachedResult: CompiledMDXProperties | undefined;
|
||||
|
||||
async function compileAndLoad() {
|
||||
if (cachedResult) return cachedResult;
|
||||
const mdxOptions = await initMdxOptions;
|
||||
const compiled = await buildMDX(collectionName, content.body, {
|
||||
...mdxOptions,
|
||||
development: false,
|
||||
frontmatter: data,
|
||||
postprocess: collection?.postprocess,
|
||||
data: {
|
||||
lastModified,
|
||||
},
|
||||
filePath: file.absolutePath,
|
||||
});
|
||||
const result = await executeMdx(String(compiled.value), {
|
||||
baseUrl: pathToFileURL(file.absolutePath),
|
||||
});
|
||||
|
||||
return (cachedResult = result as CompiledMDXProperties);
|
||||
}
|
||||
|
||||
return {
|
||||
...data,
|
||||
_file: file,
|
||||
get content() {
|
||||
return `${content.matter}${content.body}`;
|
||||
},
|
||||
async getText(type) {
|
||||
if (type === 'raw') {
|
||||
return `${content.matter}${content.body}`;
|
||||
}
|
||||
|
||||
const out = await compileAndLoad();
|
||||
if (typeof out._markdown !== 'string') missingProcessedMarkdown();
|
||||
return out._markdown;
|
||||
},
|
||||
async load() {
|
||||
const mdxOptions = await initMdxOptions;
|
||||
const out = await buildMDX(collection, content.body, {
|
||||
...mdxOptions,
|
||||
development: false,
|
||||
frontmatter: data as Record<string, unknown>,
|
||||
data: {
|
||||
lastModified,
|
||||
},
|
||||
filePath: file.absolutePath,
|
||||
});
|
||||
const { default: body, ...rest } = await executeMdx(String(out), {
|
||||
baseUrl: pathToFileURL(file.absolutePath),
|
||||
});
|
||||
const out = await compileAndLoad();
|
||||
|
||||
return {
|
||||
structuredData: out.data.structuredData as StructuredData,
|
||||
body,
|
||||
...rest,
|
||||
_exports: out as unknown as Record<string, unknown>,
|
||||
body: out.default,
|
||||
lastModified,
|
||||
};
|
||||
toc: out.toc,
|
||||
extractedReferences: out.extractedReferences,
|
||||
structuredData: out.structuredData,
|
||||
} satisfies DocData;
|
||||
},
|
||||
};
|
||||
} satisfies AsyncDocOut;
|
||||
}) as any;
|
||||
},
|
||||
docs(docs, metas, collection, config) {
|
||||
|
||||
@@ -4,32 +4,51 @@ import {
|
||||
type PageData,
|
||||
type Source,
|
||||
} from 'fumadocs-core/source';
|
||||
import type { BaseCollectionEntry, FileInfo, Runtime } from './types';
|
||||
import fs from 'node:fs';
|
||||
import type { DocOut, FileInfo, Runtime } from './types';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import type { CompiledMDXProperties } from '@/utils/build-mdx';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { missingProcessedMarkdown } from '@/runtime/shared';
|
||||
|
||||
export interface BaseCollectionEntry {
|
||||
_file: FileInfo;
|
||||
}
|
||||
|
||||
const cache = new Map<string, string>();
|
||||
|
||||
export const _runtime: Runtime = {
|
||||
doc(files) {
|
||||
return files.map((file) => {
|
||||
const { default: body, frontmatter, ...exports } = file.data;
|
||||
const data = file.data as unknown as CompiledMDXProperties;
|
||||
const filePath = file.info.absolutePath;
|
||||
|
||||
return {
|
||||
body,
|
||||
...exports,
|
||||
...(frontmatter as object),
|
||||
_file: file.info,
|
||||
_exports: file.data,
|
||||
_exports: data as unknown as Record<string, unknown>,
|
||||
body: data.default,
|
||||
lastModified: data.lastModified,
|
||||
toc: data.toc,
|
||||
structuredData: data.structuredData,
|
||||
extractedReferences: data.extractedReferences,
|
||||
...data.frontmatter,
|
||||
get content() {
|
||||
const path = (this as { _file: FileInfo })._file.absolutePath;
|
||||
const cached = cache.get(path);
|
||||
const cached = cache.get(filePath);
|
||||
if (cached) return cached;
|
||||
|
||||
const content = fs.readFileSync(path).toString();
|
||||
cache.set(path, content);
|
||||
const content = readFileSync(filePath).toString();
|
||||
cache.set(filePath, content);
|
||||
return content;
|
||||
},
|
||||
};
|
||||
async getText(type) {
|
||||
console.log(data);
|
||||
if (type === 'raw') {
|
||||
return (await readFile(filePath)).toString();
|
||||
}
|
||||
|
||||
if (typeof data._markdown !== 'string') missingProcessedMarkdown();
|
||||
return data._markdown;
|
||||
},
|
||||
} satisfies DocOut;
|
||||
}) as any;
|
||||
},
|
||||
meta(files) {
|
||||
@@ -103,3 +122,5 @@ export function resolveFiles({ docs, meta }: ResolveOptions): VirtualFile[] {
|
||||
|
||||
return outputs;
|
||||
}
|
||||
|
||||
export type * from './types';
|
||||
|
||||
@@ -2,25 +2,16 @@ import type { StandardSchemaV1 } from '@standard-schema/spec';
|
||||
import type { MetaData, PageData, Source } from 'fumadocs-core/source';
|
||||
import type { LoadedConfig } from '@/utils/config';
|
||||
import type { DocCollection, DocsCollection, MetaCollection } from '@/config';
|
||||
import type { CompiledMDXProperties } from '@/utils/build-mdx';
|
||||
|
||||
export interface BaseCollectionEntry {
|
||||
/**
|
||||
* Raw file path of collection entry, including absolute path (not normalized).
|
||||
*/
|
||||
_file: FileInfo;
|
||||
}
|
||||
import type {
|
||||
AsyncDocCollectionEntry,
|
||||
DocCollectionEntry,
|
||||
} from '@/runtime/shared';
|
||||
|
||||
export interface FileInfo {
|
||||
path: string;
|
||||
absolutePath: string;
|
||||
}
|
||||
|
||||
export interface MarkdownProps
|
||||
extends Omit<CompiledMDXProperties, 'frontmatter' | 'default'> {
|
||||
body: CompiledMDXProperties['default'];
|
||||
}
|
||||
|
||||
export interface RuntimeFile {
|
||||
info: FileInfo;
|
||||
data: Record<string, unknown>;
|
||||
@@ -33,25 +24,15 @@ export interface AsyncRuntimeFile {
|
||||
lastModified?: Date;
|
||||
}
|
||||
|
||||
type DocOut<Schema extends StandardSchemaV1> = Override<
|
||||
MarkdownProps & {
|
||||
/**
|
||||
* Other exports in the compiled Markdown/MDX file
|
||||
*/
|
||||
_exports: Record<string, unknown>;
|
||||
export type DocOut<Schema extends StandardSchemaV1 = StandardSchemaV1> =
|
||||
DocCollectionEntry<StandardSchemaV1.InferOutput<Schema>> & {
|
||||
_file: FileInfo;
|
||||
};
|
||||
|
||||
/**
|
||||
* Read the original content of file from file system.
|
||||
*/
|
||||
get content(): string;
|
||||
},
|
||||
StandardSchemaV1.InferOutput<Schema> & BaseCollectionEntry
|
||||
>;
|
||||
|
||||
type Override<A, B> = Omit<A, keyof B> & B;
|
||||
|
||||
type MetaOut<Schema extends StandardSchemaV1> =
|
||||
StandardSchemaV1.InferOutput<Schema> & BaseCollectionEntry;
|
||||
export type MetaOut<Schema extends StandardSchemaV1> =
|
||||
StandardSchemaV1.InferOutput<Schema> & {
|
||||
_file: FileInfo;
|
||||
};
|
||||
|
||||
export interface Runtime {
|
||||
doc: <C>(
|
||||
@@ -80,12 +61,10 @@ export interface Runtime {
|
||||
: never;
|
||||
}
|
||||
|
||||
type AsyncDocOut<Schema extends StandardSchemaV1> =
|
||||
StandardSchemaV1.InferOutput<Schema> &
|
||||
BaseCollectionEntry & {
|
||||
content: string;
|
||||
load: () => Promise<MarkdownProps & Record<string, unknown>>;
|
||||
};
|
||||
export type AsyncDocOut<Schema extends StandardSchemaV1 = StandardSchemaV1> =
|
||||
AsyncDocCollectionEntry<StandardSchemaV1.InferOutput<Schema>> & {
|
||||
_file: FileInfo;
|
||||
};
|
||||
|
||||
export interface RuntimeAsync {
|
||||
doc: <C>(
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { StructuredData } from 'fumadocs-core/mdx-plugins';
|
||||
import type { TableOfContents } from 'fumadocs-core/server';
|
||||
import type { FC } from 'react';
|
||||
import type { MDXProps } from 'mdx/types';
|
||||
import type { ExtractedReference } from '@/mdx-plugins/remark-postprocess';
|
||||
|
||||
export interface DocData {
|
||||
/**
|
||||
* Compiled MDX content (as component)
|
||||
*/
|
||||
body: FC<MDXProps>;
|
||||
|
||||
/**
|
||||
* table of contents generated from content.
|
||||
*/
|
||||
toc: TableOfContents;
|
||||
|
||||
/**
|
||||
* structured data for document search indexing.
|
||||
*/
|
||||
structuredData: StructuredData;
|
||||
|
||||
/**
|
||||
* Raw exports from the compiled MDX file.
|
||||
*/
|
||||
_exports: Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Last modified date of document file, obtained from version control.
|
||||
*
|
||||
* Only available when `lastModifiedTime` is enabled on global config.
|
||||
*/
|
||||
lastModified?: Date;
|
||||
|
||||
/**
|
||||
* extracted references (e.g. hrefs, paths), useful for analyzing relationships between pages.
|
||||
*/
|
||||
extractedReferences?: ExtractedReference[];
|
||||
}
|
||||
|
||||
export interface DocMethods {
|
||||
/**
|
||||
* Read the original content of file from file system.
|
||||
*
|
||||
* @deprecated use `await getText('raw')` instead.
|
||||
*/
|
||||
get content(): string;
|
||||
|
||||
/**
|
||||
* get document as text.
|
||||
*
|
||||
* - `type: raw` - read the original content from file system.
|
||||
* - `type: processed` - get the processed Markdown content, only available when `includeProcessedMarkdown` is enabled on collection config.
|
||||
*/
|
||||
getText: (type: 'raw' | 'processed') => Promise<string>;
|
||||
}
|
||||
|
||||
export type DocCollectionEntry<Frontmatter> = Override<
|
||||
DocData & DocMethods,
|
||||
Frontmatter
|
||||
>;
|
||||
|
||||
export type AsyncDocCollectionEntry<Frontmatter> = Override<
|
||||
DocMethods & {
|
||||
load: () => Promise<DocData>;
|
||||
},
|
||||
Frontmatter
|
||||
>;
|
||||
|
||||
type Override<A, B> = Omit<A, keyof B> & B;
|
||||
|
||||
export function missingProcessedMarkdown(): never {
|
||||
throw new Error(
|
||||
"getText('processed') requires `includeProcessedMarkdown` to be enabled in your collection config.",
|
||||
);
|
||||
}
|
||||
@@ -5,11 +5,15 @@ import type {
|
||||
Source,
|
||||
VirtualFile,
|
||||
} from 'fumadocs-core/source';
|
||||
import type { CompiledMDXProperties } from '@/utils/build-mdx';
|
||||
import type { FC } from 'react';
|
||||
import type { MDXProps } from 'mdx/types';
|
||||
import { type BaseCreate, fromConfigBase } from '@/runtime/vite/base';
|
||||
import * as path from 'node:path';
|
||||
import {
|
||||
AsyncDocCollectionEntry,
|
||||
DocCollectionEntry,
|
||||
DocData,
|
||||
missingProcessedMarkdown,
|
||||
} from '@/runtime/shared';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
// for server-side usage of renderers
|
||||
export { createClientLoader, toClientRenderer } from './browser';
|
||||
@@ -17,39 +21,13 @@ export type { ClientLoader, ClientLoaderOptions } from './browser';
|
||||
|
||||
export type { CompiledMDXFile } from './types';
|
||||
|
||||
type Override<A, B> = Omit<A, keyof B> & B;
|
||||
|
||||
type MDXFileToPageData<Frontmatter> = Override<
|
||||
Omit<CompiledMDXProperties<Frontmatter>, 'frontmatter' | 'default'>,
|
||||
Frontmatter & {
|
||||
/**
|
||||
* @deprecated use `body` instead.
|
||||
*/
|
||||
default: FC<MDXProps>;
|
||||
|
||||
_exports: Record<string, unknown>;
|
||||
body: FC<MDXProps>;
|
||||
}
|
||||
>;
|
||||
|
||||
type MDXFileToPageDataLazy<Frontmatter> = Override<
|
||||
Frontmatter,
|
||||
{
|
||||
load: () => Promise<
|
||||
Omit<CompiledMDXFile<Frontmatter>, 'default'> & {
|
||||
body: FC<MDXProps>;
|
||||
}
|
||||
>;
|
||||
}
|
||||
>;
|
||||
|
||||
export interface ServerCreate<Config> extends BaseCreate<Config> {
|
||||
sourceAsync: <DocOut extends PageData, MetaOut extends MetaData>(
|
||||
doc: DocMap<DocOut>,
|
||||
meta: MetaMap<MetaOut>,
|
||||
) => Promise<
|
||||
Source<{
|
||||
pageData: MDXFileToPageData<DocOut>;
|
||||
pageData: DocCollectionEntry<DocOut>;
|
||||
metaData: MetaOut;
|
||||
}>
|
||||
>;
|
||||
@@ -59,7 +37,7 @@ export interface ServerCreate<Config> extends BaseCreate<Config> {
|
||||
meta: MetaMap<MetaOut>,
|
||||
) => Promise<
|
||||
Source<{
|
||||
pageData: MDXFileToPageDataLazy<DocOut>;
|
||||
pageData: AsyncDocCollectionEntry<DocOut>;
|
||||
metaData: MetaOut;
|
||||
}>
|
||||
>;
|
||||
@@ -67,29 +45,59 @@ export interface ServerCreate<Config> extends BaseCreate<Config> {
|
||||
|
||||
export function fromConfig<Config>(): ServerCreate<Config> {
|
||||
const base = fromConfigBase<Config>();
|
||||
function mapPageData<Frontmatter>(entry: CompiledMDXFile<Frontmatter>) {
|
||||
const { toc, structuredData, lastModified, frontmatter } = entry;
|
||||
|
||||
function mapDocData(entry: CompiledMDXFile<any>): DocData {
|
||||
return {
|
||||
...frontmatter,
|
||||
default: entry.default,
|
||||
body: entry.default,
|
||||
toc,
|
||||
structuredData,
|
||||
lastModified,
|
||||
toc: entry.toc,
|
||||
extractedReferences: entry.extractedReferences,
|
||||
structuredData: entry.structuredData,
|
||||
lastModified: entry.lastModified,
|
||||
_exports: entry,
|
||||
} as MDXFileToPageData<Frontmatter>;
|
||||
};
|
||||
}
|
||||
|
||||
function mapPageData<Frontmatter>(
|
||||
filePath: string,
|
||||
entry: CompiledMDXFile<Frontmatter>,
|
||||
): DocCollectionEntry<Frontmatter> {
|
||||
return {
|
||||
...mapDocData(entry),
|
||||
get content(): string {
|
||||
throw new Error('not implemented on Vite.');
|
||||
},
|
||||
async getText(type) {
|
||||
if (type === 'raw') {
|
||||
return (await fs.readFile(filePath)).toString();
|
||||
}
|
||||
|
||||
if (typeof entry._markdown !== 'string') missingProcessedMarkdown();
|
||||
return entry._markdown;
|
||||
},
|
||||
...entry.frontmatter,
|
||||
};
|
||||
}
|
||||
|
||||
function mapPageDataLazy<Frontmatter>(
|
||||
filePath: string,
|
||||
head: Frontmatter,
|
||||
content: () => Promise<CompiledMDXFile<Frontmatter>>,
|
||||
): MDXFileToPageDataLazy<Frontmatter> {
|
||||
): AsyncDocCollectionEntry<Frontmatter> {
|
||||
return {
|
||||
...head,
|
||||
async load() {
|
||||
const { default: body, ...rest } = await content();
|
||||
return { body, ...rest };
|
||||
return mapDocData(await content());
|
||||
},
|
||||
get content(): string {
|
||||
throw new Error('not implemented on Vite.');
|
||||
},
|
||||
async getText(type) {
|
||||
if (type === 'raw') {
|
||||
return (await fs.readFile(filePath)).toString();
|
||||
}
|
||||
|
||||
const entry = await content();
|
||||
if (typeof entry._markdown !== 'string') missingProcessedMarkdown();
|
||||
return entry._markdown;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -100,11 +108,13 @@ export function fromConfig<Config>(): ServerCreate<Config> {
|
||||
async sourceAsync(doc, meta) {
|
||||
const virtualFiles: Promise<VirtualFile>[] = [
|
||||
...Object.entries(doc).map(async ([file, content]) => {
|
||||
const fullPath = path.join(content.base, file);
|
||||
|
||||
return {
|
||||
type: 'page',
|
||||
path: file,
|
||||
absolutePath: path.join(content.base, file),
|
||||
data: mapPageData(await content()),
|
||||
absolutePath: fullPath,
|
||||
data: mapPageData(fullPath, await content()),
|
||||
} satisfies VirtualFile;
|
||||
}),
|
||||
...Object.entries(meta).map(async ([file, content]) => {
|
||||
@@ -122,11 +132,17 @@ export function fromConfig<Config>(): ServerCreate<Config> {
|
||||
async sourceLazy(doc, meta) {
|
||||
const virtualFiles: Promise<VirtualFile>[] = [
|
||||
...Object.entries(doc.head).map(async ([file, frontmatter]) => {
|
||||
const fullPath = path.join(doc.base, file);
|
||||
|
||||
return {
|
||||
type: 'page',
|
||||
path: file,
|
||||
absolutePath: path.join(doc.base, file),
|
||||
data: mapPageDataLazy(await frontmatter(), doc.body[file]),
|
||||
absolutePath: fullPath,
|
||||
data: mapPageDataLazy(
|
||||
fullPath,
|
||||
await frontmatter(),
|
||||
doc.body[file],
|
||||
),
|
||||
} satisfies VirtualFile;
|
||||
}),
|
||||
...Object.entries(meta).map(async ([file, content]) => {
|
||||
|
||||
@@ -5,7 +5,12 @@ import type { StructuredData } from 'fumadocs-core/mdx-plugins';
|
||||
import type { TableOfContents } from 'fumadocs-core/server';
|
||||
import type { FC } from 'react';
|
||||
import type { MDXProps } from 'mdx/types';
|
||||
import type { ExtractedReference } from '@/mdx-plugins/remark-postprocess';
|
||||
import {
|
||||
type ExtractedReference,
|
||||
type PostprocessOptions,
|
||||
remarkPostprocess,
|
||||
} from '@/mdx-plugins/remark-postprocess';
|
||||
import { remarkMdxExport } from '@/mdx-plugins/remark-mdx-export';
|
||||
|
||||
type Processor = ReturnType<typeof createProcessor>;
|
||||
|
||||
@@ -25,6 +30,7 @@ interface BuildMDXOptions extends ProcessorOptions {
|
||||
data?: Record<string, unknown>;
|
||||
|
||||
_compiler?: CompilerOptions;
|
||||
postprocess?: PostprocessOptions;
|
||||
}
|
||||
|
||||
export interface CompilerOptions {
|
||||
@@ -42,6 +48,7 @@ export interface CompiledMDXProperties<Frontmatter = Record<string, unknown>> {
|
||||
*/
|
||||
lastModified?: Date;
|
||||
extractedReferences?: ExtractedReference[];
|
||||
_markdown?: string;
|
||||
}
|
||||
|
||||
export type { DataMap };
|
||||
@@ -82,7 +89,23 @@ export async function buildMDX(
|
||||
processor = createProcessor({
|
||||
outputFormat: 'program',
|
||||
...rest,
|
||||
remarkPlugins: [remarkInclude, ...(rest.remarkPlugins ?? [])],
|
||||
remarkPlugins: [
|
||||
remarkInclude,
|
||||
...(rest.remarkPlugins ?? []),
|
||||
[remarkPostprocess, options.postprocess],
|
||||
[
|
||||
remarkMdxExport,
|
||||
{
|
||||
dataNames: [
|
||||
'structuredData',
|
||||
'extractedReferences',
|
||||
'frontmatter',
|
||||
'lastModified',
|
||||
'_markdown',
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
format,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import * as plugins from 'fumadocs-core/mdx-plugins';
|
||||
import type { ProcessorOptions } from '@mdx-js/mdx';
|
||||
import type { Pluggable } from 'unified';
|
||||
import {
|
||||
type PostprocessOptions,
|
||||
remarkPostprocess,
|
||||
} from '@/mdx-plugins/remark-postprocess';
|
||||
|
||||
type ResolvePlugins = Pluggable[] | ((v: Pluggable[]) => Pluggable[]);
|
||||
|
||||
@@ -56,14 +52,6 @@ export function getDefaultMDXOptions({
|
||||
_withoutBundler = false,
|
||||
...mdxOptions
|
||||
}: DefaultMDXOptions): ProcessorOptions {
|
||||
const mdxExports = [
|
||||
'structuredData',
|
||||
'extractedReferences',
|
||||
'frontmatter',
|
||||
'lastModified',
|
||||
...valueToExport,
|
||||
];
|
||||
|
||||
const remarkPlugins = pluginOption(
|
||||
(v) => [
|
||||
plugins.remarkGfm,
|
||||
@@ -93,10 +81,16 @@ export function getDefaultMDXOptions({
|
||||
plugins.remarkStructure,
|
||||
remarkStructureOptions,
|
||||
],
|
||||
[
|
||||
remarkPostprocess,
|
||||
{ injectExports: mdxExports } satisfies PostprocessOptions,
|
||||
],
|
||||
() => {
|
||||
return (_, file) => {
|
||||
file.data['mdx-export'] ??= [];
|
||||
|
||||
for (const name of valueToExport) {
|
||||
if (name in file.data)
|
||||
file.data['mdx-export'].push({ name, value: file.data[name] });
|
||||
}
|
||||
};
|
||||
},
|
||||
],
|
||||
mdxOptions.remarkPlugins,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user