MDX: Support defineDocs without re-exporting docs and meta collections

This commit is contained in:
Fuma Nama
2025-02-05 18:18:44 +08:00
parent b8bd0657c6
commit 432c7bda75
16 changed files with 403 additions and 282 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-mdx': minor
---
Support `defineDocs` without re-exporting `docs` and `meta` collections
+4 -1
View File
@@ -1,8 +1,11 @@
@import 'tailwindcss';
@import 'tailwindcss' source(none);
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
@import 'fumadocs-twoslash/twoslash.css';
@source ".";
@source "../components";
@source "../content";
@source '../node_modules/fumadocs-ui/dist/**/*.js';
@source '../node_modules/fumadocs-openapi/dist/**/*.js';
@plugin 'tailwindcss-animate';
+2 -2
View File
@@ -4,7 +4,7 @@ import { loader } from 'fumadocs-core/source';
import { icons } from 'lucide-react';
import { attachFile, createOpenAPI } from 'fumadocs-openapi/server';
import { createElement } from 'react';
import { meta, docs, blog as blogPosts } from '@/.source';
import { docs, blog as blogPosts } from '@/.source';
export const source = loader({
baseUrl: '/docs',
@@ -12,7 +12,7 @@ export const source = loader({
if (icon && icon in icons)
return createElement(icons[icon as keyof typeof icons]);
},
source: createMDXSource(docs, meta),
source: docs.toFumadocsSource(),
pageTree: {
attachFile,
},
+1 -1
View File
@@ -17,7 +17,7 @@ import rehypeKatex from 'rehype-katex';
import { z } from 'zod';
import { rehypeCodeDefaultOptions } from 'fumadocs-core/mdx-plugins';
export const { docs, meta } = defineDocs({
export const docs = defineDocs({
docs: {
async: true,
schema: frontmatterSchema.extend({
+16 -11
View File
@@ -1,10 +1,11 @@
import type { GlobalConfig } from '@/config/types';
import type { ProcessorOptions } from '@mdx-js/mdx';
import {
InternalDocCollection,
InternalMetaCollection,
LoadedConfig,
} from '@/utils/load-config';
import type { LoadedConfig } from '@/utils/load-config';
import type {
DocsCollection,
DocCollection,
MetaCollection,
} from '@/config/define';
export function buildConfig(
config: Record<string, unknown>,
@@ -17,12 +18,16 @@ export function buildConfig(
continue;
}
if (typeof v === 'object' && '_doc' in v && v._doc === 'collections') {
collections.set(
k,
v as unknown as InternalMetaCollection | InternalDocCollection,
);
continue;
if (typeof v === 'object' && 'type' in v) {
if (v.type === 'docs') {
collections.set(k, v as DocsCollection);
continue;
}
if (v.type === 'doc' || v.type === 'meta') {
collections.set(k, v as unknown as MetaCollection | DocCollection);
continue;
}
}
if (k === 'default') {
+13 -23
View File
@@ -1,12 +1,8 @@
import { type ProcessorOptions } from '@mdx-js/mdx';
import { type MDXOptions } from '@/utils/build-mdx';
import {
type BaseCollectionEntry,
type GlobalConfig,
type MarkdownProps,
} from '@/config/types';
import { type GlobalConfig } from '@/config/types';
import { frontmatterSchema, metaSchema } from '@/utils/schema';
import { StandardSchemaV1 } from '@standard-schema/spec';
import type { StandardSchemaV1 } from '@standard-schema/spec';
export interface TransformContext {
path: string;
@@ -54,6 +50,12 @@ export interface DocCollection<
async?: Async;
}
export interface DocsCollection {
type: 'docs';
docs: DocCollection;
meta: MetaCollection;
}
export function defineCollections<
T extends 'doc' | 'meta',
Schema extends StandardSchemaV1 = StandardSchemaV1,
@@ -63,28 +65,14 @@ export function defineCollections<
? DocCollection<Schema, Async>
: MetaCollection<Schema>),
): {
_doc: 'collections';
type: T;
_type: {
async: Async;
runtime: T extends 'doc'
? Async extends true
? StandardSchemaV1.InferOutput<Schema> &
BaseCollectionEntry & {
load: () => Promise<MarkdownProps>;
}
: Omit<MarkdownProps, keyof StandardSchemaV1.InferOutput<Schema>> &
StandardSchemaV1.InferOutput<Schema> &
BaseCollectionEntry
: typeof options extends MetaCollection
? StandardSchemaV1.InferOutput<Schema> & BaseCollectionEntry
: never;
schema: Schema;
};
} {
return {
_doc: 'collections',
// @ts-expect-error -- internal type inferring
_type: undefined,
...options,
@@ -103,15 +91,17 @@ export function defineDocs<
*/
dir?: string | string[];
docs?: Partial<DocCollection<DocData, DocAsync>>;
meta?: Partial<MetaCollection<MetaData>>;
docs?: Omit<DocCollection<DocData, DocAsync>, 'dir' | 'type'>;
meta?: Omit<MetaCollection<MetaData>, 'dir' | 'type'>;
}): {
type: 'docs';
docs: ReturnType<typeof defineCollections<'doc', DocData, DocAsync>>;
meta: ReturnType<typeof defineCollections<'meta', MetaData, false>>;
} {
const dir = options?.dir ?? 'content/docs';
return {
type: 'docs',
docs: defineCollections({
type: 'doc',
dir,
+4 -21
View File
@@ -1,9 +1,9 @@
import { type AnyZodObject, type z } from 'zod';
import type { MDXProps } from 'mdx/types';
import type { StructuredData } from 'fumadocs-core/mdx-plugins';
import type { TableOfContents } from 'fumadocs-core/server';
import { type DefaultMDXOptions } from '@/utils/mdx-options';
import type { FC } from 'react';
import type { StandardSchemaV1 } from '@standard-schema/spec';
export interface GlobalConfig {
/**
@@ -37,7 +37,9 @@ export type InferSchema<CollectionOut> = CollectionOut extends {
: never;
export type InferSchemaType<C> =
InferSchema<C> extends AnyZodObject ? z.output<InferSchema<C>> : never;
InferSchema<C> extends StandardSchemaV1
? StandardSchemaV1.InferOutput<InferSchema<C>>
: never;
export interface FileInfo {
path: string;
@@ -56,25 +58,6 @@ export interface MarkdownProps {
lastModified?: Date;
}
export type CollectionEntry<
CollectionOut extends {
_type: {
runtime: unknown;
};
},
> = CollectionOut['_type']['runtime'];
export interface BaseCollectionEntry {
_file: FileInfo;
}
/**
* Get output type of collections
*/
export type GetOutput<
C extends {
_type: {
runtime: unknown;
};
},
> = CollectionEntry<C>[];
+1
View File
@@ -61,6 +61,7 @@ export default async function loader(
? config.collections.get(collectionId)
: undefined;
if (collection && collection.type === 'docs') collection = collection.docs;
if (collection && collection.type !== 'doc') {
collection = undefined;
}
+68 -73
View File
@@ -2,26 +2,28 @@ import * as path from 'node:path';
import fg from 'fast-glob';
import { getTypeFromPath } from '@/utils/get-type-from-path';
import type { FileInfo } from '@/config/types';
import {
type InternalDocCollection,
type InternalMetaCollection,
type LoadedConfig,
} from '@/utils/load-config';
import { type LoadedConfig } from '@/utils/load-config';
import type { DocCollection, MetaCollection } from '@/config';
export async function generateJS(
configPath: string,
config: LoadedConfig,
outputPath: string,
configHash: string,
getFrontmatter: (file: string) => Promise<unknown>,
getFrontmatter: (file: string) => unknown | Promise<unknown>,
): Promise<string> {
const outDir = path.dirname(outputPath);
const imports: ImportInfo[] = [
{
type: 'named',
names: ['toRuntime'],
names: ['_runtime'],
specifier: 'fumadocs-mdx',
},
{
type: 'namespace',
specifier: toImportPath(configPath, outDir),
name: '_source',
},
];
let asyncInit = false;
const lines: string[] = [];
@@ -29,59 +31,73 @@ export async function generateJS(
config._runtime.files.clear();
const entries = Array.from(config.collections.entries());
const declares = entries.map(async ([k, collection]) => {
const files = await getCollectionFiles(collection);
if (collection.type === 'doc' && collection.async) {
if (!asyncInit) {
imports.push(
{
type: 'namespace',
specifier: toImportPath(configPath, outDir),
name: '_source',
},
{
type: 'named',
specifier: 'fumadocs-mdx/runtime/async',
names: ['asyncFiles', 'buildConfig'],
},
);
lines.unshift(
'const [err, _sourceConfig] = buildConfig(_source)',
'if (err) throw new Error(err)',
);
asyncInit = true;
}
const entries = files.map(async (file) => {
const frontmatter = await getFrontmatter(file.absolutePath);
return JSON.stringify({
frontmatter,
file,
});
});
return `export const ${k} = asyncFiles([${(await Promise.all(entries)).join(', ')}], "${k}", _sourceConfig)`;
}
async function getEntries(collectionName: string, files: FileInfo[]) {
const items = files.map(async (file, i) => {
config._runtime.files.set(file.absolutePath, k);
const importId = `${k}_${i}`;
config._runtime.files.set(file.absolutePath, collectionName);
const importId = `${collectionName}_${i}`;
imports.push({
type: 'namespace',
name: importId,
specifier: `${toImportPath(file.absolutePath, outDir)}?collection=${k}&hash=${configHash}`,
specifier: `${toImportPath(file.absolutePath, outDir)}?collection=${collectionName}&hash=${configHash}`,
});
return `toRuntime("${collection.type}", ${importId}, ${JSON.stringify(file)})`;
return `{ info: ${JSON.stringify(file)}, data: ${importId} }`;
});
const resolvedItems = await Promise.all(items);
return `export const ${k} = [${resolvedItems.join(', ')}];`;
return Promise.all(items);
}
async function getAsyncEntries(files: FileInfo[]) {
if (!asyncInit) {
imports.push({
type: 'named',
specifier: 'fumadocs-mdx/runtime/async',
names: ['_runtimeAsync', 'buildConfig'],
});
lines.unshift(
'const [err, _sourceConfig] = buildConfig(_source)',
'if (err) throw new Error(err)',
);
asyncInit = true;
}
const entries = files.map(async (file) => {
const frontmatter = await getFrontmatter(file.absolutePath);
return JSON.stringify({
info: file,
data: frontmatter,
});
});
return Promise.all(entries);
}
const declares = entries.map(async ([k, collection]) => {
if (collection.type === 'docs') {
const docs = await getCollectionFiles(collection.docs);
const metas = await getCollectionFiles(collection.meta);
if (collection.docs.async) {
const docsEntries = (await getAsyncEntries(docs)).join(', ');
const metaEntries = (await getEntries(k, metas)).join(', ');
return `export const ${k} = _runtimeAsync.docs<typeof _source.${k}>([${docsEntries}], [${metaEntries}], "${k}", _sourceConfig)`;
}
return `export const ${k} = _runtime.docs<typeof _source.${k}>([${(await getEntries(k, docs)).join(', ')}], [${(await getEntries(k, metas)).join(', ')}])`;
}
const files = await getCollectionFiles(collection);
if (collection.type === 'doc' && collection.async) {
return `export const ${k} = _runtimeAsync.doc<typeof _source.${k}>([${(await getAsyncEntries(files)).join(', ')}], "${k}", _sourceConfig)`;
}
return `export const ${k} = _runtime.${collection.type}<typeof _source.${k}>([${(await getEntries(k, files)).join(', ')}]);`;
});
const resolvedDeclares = await Promise.all(declares);
@@ -92,7 +108,7 @@ export async function generateJS(
}
async function getCollectionFiles(
collection: InternalDocCollection | InternalMetaCollection,
collection: DocCollection | MetaCollection,
): Promise<FileInfo[]> {
const files = new Map<string, FileInfo>();
const dirs = Array.isArray(collection.dir)
@@ -163,24 +179,3 @@ export function toImportPath(file: string, dir: string): string {
return importPath.replaceAll(path.sep, '/');
}
export function generateTypes(
configPath: string,
config: LoadedConfig,
outputPath: string,
): string {
const importPath = JSON.stringify(
toImportPath(configPath, path.dirname(outputPath)),
);
const lines: string[] = [
'import type { GetOutput } from "fumadocs-mdx/config"',
];
for (const name of config.collections.keys()) {
lines.push(
`export declare const ${name}: GetOutput<typeof import(${importPath}).${name}>`,
);
}
return lines.join('\n');
}
+13 -54
View File
@@ -1,9 +1,9 @@
import * as path from 'node:path';
import * as fs from 'node:fs';
import { writeFile } from 'node:fs/promises';
import grayMatter from 'gray-matter';
import { getConfigHash, loadConfigCached } from '@/utils/config-cache';
import { generateJS, generateTypes } from '@/map/generate';
import { generateJS } from '@/map/generate';
import { readFrontmatter } from '@/utils/read-frontmatter';
/**
* Start a MDX server that builds index and manifest files.
@@ -17,64 +17,25 @@ export async function start(
): Promise<void> {
let configHash = await getConfigHash(configPath);
let config = await loadConfigCached(configPath, configHash);
const jsOut = path.resolve(outDir, `index.js`);
const typeOut = path.resolve(outDir, `index.d.ts`);
const jsOut = path.resolve(outDir, `index.ts`);
const frontmatterCache = new Map<string, unknown>();
let hookUpdate = false;
/**
* Read frontmatter via stream, it is faster for large Markdown/MDX files
*/
async function readFrontmatter(file: string): Promise<unknown> {
hookUpdate = true;
const cached = frontmatterCache.get(file);
if (cached) return cached;
const readStream = fs.createReadStream(file, {
highWaterMark: 250,
});
return new Promise((res, rej) => {
let idx = 0;
let str = '';
readStream.on('data', (_chunk) => {
const chunk = _chunk.toString();
if (idx === 0 && !chunk.startsWith('---')) {
res({});
readStream.close();
return;
}
str += chunk;
idx++;
if (str.includes('\n---')) {
res(
grayMatter({
content: str,
}).data,
);
readStream.close();
}
});
readStream.on('end', () => res({}));
readStream.on('error', (e) => rej(e));
}).then((res) => {
frontmatterCache.set(file, res);
return res;
});
}
fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(
jsOut,
await generateJS(configPath, config, jsOut, configHash, readFrontmatter),
await generateJS(configPath, config, jsOut, configHash, (file) => {
hookUpdate = true;
const cached = frontmatterCache.get(file);
if (cached) return cached;
return readFrontmatter(file).then((res) => {
frontmatterCache.set(file, res);
return res;
});
}),
);
fs.writeFileSync(typeOut, generateTypes(configPath, config, typeOut));
console.log('[MDX] initialized map file');
if (dev) {
@@ -94,8 +55,6 @@ export async function start(
if (isConfigFile) {
configHash = await getConfigHash(configPath);
config = await loadConfigCached(configPath, configHash);
await writeFile(typeOut, generateTypes(configPath, config, typeOut));
console.log('[MDX] Updated map types');
}
if (isConfigFile || event !== 'change' || hookUpdate) {
+10 -2
View File
@@ -4,9 +4,17 @@ import { type LoadedConfig } from '@/utils/load-config';
export function watcher(configPath: string, config: LoadedConfig): FSWatcher {
const deps: string[] = [configPath];
function add(dir: string | string[]) {
if (Array.isArray(dir)) deps.push(...dir);
else deps.push(dir);
}
for (const collection of config.collections.values()) {
if (Array.isArray(collection.dir)) deps.push(...collection.dir);
else deps.push(collection.dir);
if (collection.type === 'docs') {
add(collection.docs.dir);
add(collection.meta.dir);
} else {
add(collection.dir);
}
}
return watch(deps, {
+16 -4
View File
@@ -1,15 +1,27 @@
import * as path from 'node:path';
import * as fs from 'node:fs';
import { findConfigFile, loadConfig } from '@/utils/load-config';
import { generateTypes } from '@/map/generate';
import { generateJS } from '@/map/generate';
import { getConfigHash } from '@/utils/config-cache';
import { readFrontmatter } from '@/utils/read-frontmatter';
export async function postInstall(
configPath = findConfigFile(),
): Promise<void> {
const typeOut = path.resolve('.source/index.d.ts');
const jsOut = path.resolve('.source/index.ts');
const config = await loadConfig(configPath);
const hash = await getConfigHash(configPath);
fs.mkdirSync(path.dirname(typeOut), { recursive: true });
fs.writeFileSync(typeOut, generateTypes(configPath, config, typeOut));
fs.mkdirSync(path.dirname(jsOut), { recursive: true });
fs.writeFileSync(
jsOut,
await generateJS(
configPath,
config,
path.resolve('.source/index.ts'),
hash,
readFrontmatter,
),
);
console.log('[MDX] types generated');
}
+109 -64
View File
@@ -1,78 +1,123 @@
import { type CollectionEntry, type FileInfo } from '@/config/types';
import { type BaseCollectionEntry, type MarkdownProps } from '@/config/types';
import { createCompiler, type MDXOptions } from '@fumadocs/mdx-remote';
import * as fs from 'node:fs/promises';
import type { z } from 'zod';
import type { LoadedConfig } from '@/utils/load-config';
import { remarkInclude } from '@/mdx-plugins/remark-include';
import type { defineCollections } from '@/config/define';
import type { frontmatterSchema } from '@/utils/schema';
import {
remarkStructure,
type StructuredData,
} from 'fumadocs-core/mdx-plugins';
import type { StandardSchemaV1 } from '@standard-schema/spec';
import { _runtime, createMDXSource, type RuntimeFile } from '@/runtime/index';
import type { MetaData, PageData, Source } from 'fumadocs-core/source';
type T = ReturnType<
typeof defineCollections<'doc', typeof frontmatterSchema, true>
>;
/**
* @internal
*/
export function asyncFiles(
files: {
file: FileInfo;
frontmatter: Record<string, unknown>;
}[],
collection: string,
config: LoadedConfig,
): unknown[] {
async function init() {
const col = config.collections.get(collection);
let mdxOptions: MDXOptions;
if (col?.type === 'doc' && col.mdxOptions) {
mdxOptions = col.mdxOptions as MDXOptions;
} else {
const options =
typeof config.global?.mdxOptions === 'function'
? await config.global.mdxOptions()
: config.global?.mdxOptions;
const remarkPlugins = options?.remarkPlugins ?? [];
mdxOptions = {
...options,
remarkPlugins: (v) =>
typeof remarkPlugins === 'function'
? [remarkInclude, ...remarkPlugins(v), remarkStructure]
: [remarkInclude, ...v, ...remarkPlugins, remarkStructure],
};
}
return createCompiler(mdxOptions);
export interface RuntimeAsync {
doc: <C>(
files: RuntimeFile[],
collection: string,
config: LoadedConfig,
) => C extends ReturnType<
typeof defineCollections<'doc', infer Schema extends StandardSchemaV1, true>
>
? (StandardSchemaV1.InferOutput<Schema> &
BaseCollectionEntry & {
load: () => Promise<MarkdownProps>;
})[]
: never;
docs: <Docs>(
docs: RuntimeFile[],
metas: RuntimeFile[],
collection: string,
config: LoadedConfig,
) => Docs extends {
type: 'docs';
docs: unknown;
meta: unknown;
}
const initCompiler = init();
return files.map(({ file, frontmatter }) => {
return {
...(frontmatter as z.infer<typeof frontmatterSchema>),
_file: file,
async load() {
const compiler = await initCompiler;
const out = await compiler.compile({
source: (await fs.readFile(file.absolutePath)).toString(),
filePath: file.absolutePath,
});
return {
body: out.body,
toc: out.toc,
structuredData: out.vfile.data.structuredData as StructuredData,
_exports: out.exports ?? {},
};
},
} satisfies CollectionEntry<T>;
});
? {
docs: ReturnType<typeof _runtimeAsync.doc<Docs['docs']>>;
meta: ReturnType<typeof _runtime.meta<Docs['meta']>>;
toFumadocsSource: () => Source<{
pageData: ReturnType<
typeof _runtimeAsync.doc<Docs['docs']>
>[number] extends PageData & BaseCollectionEntry
? ReturnType<typeof _runtimeAsync.doc<Docs['docs']>>[number]
: never;
metaData: ReturnType<
typeof _runtime.meta<Docs['meta']>
>[number] extends MetaData & BaseCollectionEntry
? ReturnType<typeof _runtime.meta<Docs['meta']>>[number]
: never;
}>;
}
: never;
}
async function initCompiler(config: LoadedConfig, collection: string) {
let col = config.collections.get(collection);
if (col?.type === 'docs') col = col.docs;
let mdxOptions: MDXOptions;
if (col?.type === 'doc' && col.mdxOptions) {
mdxOptions = col.mdxOptions as MDXOptions;
} else {
const options =
typeof config.global?.mdxOptions === 'function'
? await config.global.mdxOptions()
: config.global?.mdxOptions;
const remarkPlugins = options?.remarkPlugins ?? [];
mdxOptions = {
...options,
remarkPlugins: (v) =>
typeof remarkPlugins === 'function'
? [remarkInclude, ...remarkPlugins(v), remarkStructure]
: [remarkInclude, ...v, ...remarkPlugins, remarkStructure],
};
}
return createCompiler(mdxOptions);
}
export const _runtimeAsync: RuntimeAsync = {
doc(files, collection, config) {
const init = initCompiler(config, collection);
return files.map(({ info: file, data: frontmatter }) => {
return {
...frontmatter,
_file: file,
async load() {
const compiler = await init;
const out = await compiler.compile({
source: (await fs.readFile(file.absolutePath)).toString(),
filePath: file.absolutePath,
});
return {
body: out.body,
toc: out.toc,
structuredData: out.vfile.data.structuredData as StructuredData,
_exports: out.exports ?? {},
};
},
};
}) as any;
},
docs(docs, metas, collection, config) {
const parsedDocs = this.doc(docs, collection, config);
const parsedMetas = _runtime.meta(metas);
return {
docs: parsedDocs,
meta: parsedMetas,
toFumadocsSource() {
return createMDXSource(parsedDocs, parsedMetas);
},
} as any;
},
};
export { buildConfig } from '@/config/build';
+94 -21
View File
@@ -3,31 +3,104 @@ import {
type PageData,
type Source,
} from 'fumadocs-core/source';
import { type BaseCollectionEntry, type FileInfo } from '@/config';
import {
type BaseCollectionEntry,
type defineCollections,
type DocsCollection,
type FileInfo,
type MarkdownProps,
} from '@/config';
import type { VirtualFile } from 'fumadocs-core/source';
import type { StandardSchemaV1 } from '@standard-schema/spec';
export function toRuntime(
type: 'doc' | 'meta',
file: Record<string, unknown>,
info: FileInfo,
): unknown {
if (type === 'doc') {
const { default: body, frontmatter, ...exports } = file;
export interface RuntimeFile {
info: FileInfo;
data: Record<string, unknown>;
}
export interface Runtime {
doc: <C>(
files: RuntimeFile[],
) => C extends ReturnType<
typeof defineCollections<
'doc',
infer Schema extends StandardSchemaV1,
false
>
>
? (Omit<MarkdownProps, keyof StandardSchemaV1.InferOutput<Schema>> &
StandardSchemaV1.InferOutput<Schema> &
BaseCollectionEntry)[]
: never;
meta: <C>(
files: RuntimeFile[],
) => C extends ReturnType<
typeof defineCollections<
'meta',
infer Schema extends StandardSchemaV1,
false
>
>
? (StandardSchemaV1.InferOutput<Schema> & BaseCollectionEntry)[]
: never;
docs: <Docs>(
docs: RuntimeFile[],
metas: RuntimeFile[],
) => Docs extends DocsCollection
? {
docs: ReturnType<typeof _runtime.doc<Docs['docs']>>;
meta: ReturnType<typeof _runtime.meta<Docs['meta']>>;
toFumadocsSource: () => Source<{
pageData: ReturnType<
typeof _runtime.doc<Docs['docs']>
>[number] extends PageData & BaseCollectionEntry
? ReturnType<typeof _runtime.doc<Docs['docs']>>[number]
: never;
metaData: ReturnType<
typeof _runtime.meta<Docs['meta']>
>[number] extends MetaData & BaseCollectionEntry
? ReturnType<typeof _runtime.meta<Docs['meta']>>[number]
: never;
}>;
}
: never;
}
export const _runtime: Runtime = {
doc(files) {
return files.map((file) => {
const { default: body, frontmatter, ...exports } = file.data;
return {
body,
...exports,
...(frontmatter as object),
_exports: file.data,
_file: file.info,
};
}) as any;
},
meta(files) {
return files.map((file) => {
return {
...(file.data.default as object),
_file: file.info,
};
}) as any;
},
docs(docs, metas) {
const parsedDocs = this.doc(docs);
const parsedMetas = this.meta(metas);
return {
body,
...exports,
...(frontmatter as object),
_exports: file,
_file: info,
};
}
return {
...(file.default as object),
_file: info,
};
}
docs: parsedDocs,
meta: parsedMetas,
toFumadocsSource() {
return createMDXSource(parsedDocs, parsedMetas);
},
} as any;
},
};
export function createMDXSource<
Doc extends PageData & BaseCollectionEntry,
+6 -5
View File
@@ -1,5 +1,9 @@
import * as path from 'node:path';
import type { DocCollection, MetaCollection } from '@/config/define';
import type {
DocCollection,
DocsCollection,
MetaCollection,
} from '@/config/define';
import { type GlobalConfig } from '@/config/types';
import type { ProcessorOptions } from '@mdx-js/mdx';
import { pathToFileURL } from 'node:url';
@@ -10,7 +14,7 @@ export function findConfigFile(): string {
}
export interface LoadedConfig {
collections: Map<string, InternalDocCollection | InternalMetaCollection>;
collections: Map<string, DocCollection | MetaCollection | DocsCollection>;
getDefaultMDXOptions: () => Promise<ProcessorOptions>;
global?: GlobalConfig;
@@ -22,9 +26,6 @@ export interface LoadedConfig {
};
}
export type InternalDocCollection = DocCollection;
export type InternalMetaCollection = MetaCollection;
export async function loadConfig(configPath: string): Promise<LoadedConfig> {
const { build } = await import('esbuild');
@@ -0,0 +1,41 @@
import * as fs from 'node:fs';
import grayMatter from 'gray-matter';
/**
* Read frontmatter via stream, it is faster for large Markdown/MDX files
*/
export async function readFrontmatter(file: string): Promise<unknown> {
const readStream = fs.createReadStream(file, {
highWaterMark: 250,
});
return new Promise((res, rej) => {
let idx = 0;
let str = '';
readStream.on('data', (_chunk) => {
const chunk = _chunk.toString();
if (idx === 0 && !chunk.startsWith('---')) {
res({});
readStream.close();
return;
}
str += chunk;
idx++;
if (str.includes('\n---')) {
res(
grayMatter({
content: str,
}).data,
);
readStream.close();
}
});
readStream.on('end', () => res({}));
readStream.on('error', (e) => rej(e));
});
}