mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 23:49:51 +00:00
MDX: Stabilize Vite plugin support
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-mdx': minor
|
||||
---
|
||||
|
||||
Stabilize Vite plugin support
|
||||
@@ -7,36 +7,49 @@ import {
|
||||
DocsTitle,
|
||||
} from 'fumadocs-ui/page';
|
||||
import { source } from '@/source';
|
||||
import { type PageTree } from 'fumadocs-core/server';
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
import { executeMdxSync } from '@fumadocs/mdx-remote/client';
|
||||
import type { PageTree } from 'fumadocs-core/server';
|
||||
import { createCompiler } from '@fumadocs/mdx-remote';
|
||||
import * as path from 'node:path';
|
||||
|
||||
const compiler = createCompiler({
|
||||
development: false,
|
||||
});
|
||||
import { docs } from '../../content/docs';
|
||||
import { toClientRenderer } from 'fumadocs-mdx/runtime/vite';
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const slugs = params['*'].split('/').filter((v) => v.length > 0);
|
||||
const page = source.getPage(slugs);
|
||||
if (!page) throw new Response('Not found', { status: 404 });
|
||||
|
||||
const compiled = await compiler.compileFile({
|
||||
path: path.resolve('content/docs', page.path),
|
||||
value: page.data.content,
|
||||
});
|
||||
|
||||
return {
|
||||
page,
|
||||
compiled: compiled.toString(),
|
||||
tree: source.pageTree,
|
||||
};
|
||||
}
|
||||
|
||||
const clientLoader = toClientRenderer(
|
||||
docs.doc,
|
||||
(
|
||||
{ toc, default: Mdx },
|
||||
{
|
||||
description,
|
||||
title,
|
||||
}: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
},
|
||||
) => {
|
||||
return (
|
||||
<DocsPage toc={toc}>
|
||||
<DocsTitle>{title}</DocsTitle>
|
||||
<DocsDescription>{description}</DocsDescription>
|
||||
<DocsBody>
|
||||
<Mdx components={{ ...defaultMdxComponents }} />
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default function Page(props: Route.ComponentProps) {
|
||||
const { page, compiled, tree } = props.loaderData;
|
||||
const { default: Mdx, toc } = executeMdxSync(compiled);
|
||||
const { tree, page } = props.loaderData;
|
||||
const Content = clientLoader[page.path];
|
||||
|
||||
return (
|
||||
<DocsLayout
|
||||
@@ -47,13 +60,7 @@ export default function Page(props: Route.ComponentProps) {
|
||||
>
|
||||
<title>{page.data.title}</title>
|
||||
<meta name="description" content={page.data.description} />
|
||||
<DocsPage toc={toc}>
|
||||
<DocsTitle>{page.data.title}</DocsTitle>
|
||||
<DocsDescription>{page.data.description}</DocsDescription>
|
||||
<DocsBody>
|
||||
<Mdx components={defaultMdxComponents} />
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
<Content {...page.data} />
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import type { Route } from './+types/search';
|
||||
import { createSearchAPI } from 'fumadocs-core/search/server';
|
||||
import { createFromSource } from 'fumadocs-core/search/server';
|
||||
import { source } from '@/source';
|
||||
import { structure } from 'fumadocs-core/mdx-plugins';
|
||||
|
||||
const server = createSearchAPI('advanced', {
|
||||
indexes: source.getPages().map((page) => ({
|
||||
id: page.url,
|
||||
url: page.url,
|
||||
title: page.data.title ?? '',
|
||||
description: page.data.description,
|
||||
structuredData: structure(page.data.content),
|
||||
})),
|
||||
const server = createFromSource(source, {
|
||||
language: 'english',
|
||||
});
|
||||
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
|
||||
@@ -1,60 +1,7 @@
|
||||
import {
|
||||
loader,
|
||||
type MetaData,
|
||||
type PageData,
|
||||
type Source,
|
||||
type VirtualFile,
|
||||
} from 'fumadocs-core/source';
|
||||
import matter from 'gray-matter';
|
||||
import * as path from 'node:path';
|
||||
|
||||
const files = Object.entries(
|
||||
import.meta.glob<true, 'raw'>('/content/docs/**/*', {
|
||||
eager: true,
|
||||
query: '?raw',
|
||||
import: 'default',
|
||||
}),
|
||||
);
|
||||
|
||||
const virtualFiles: VirtualFile[] = files.flatMap(([file, content]) => {
|
||||
const ext = path.extname(file);
|
||||
const virtualPath = path.relative(
|
||||
'content/docs',
|
||||
path.join(process.cwd(), file),
|
||||
);
|
||||
|
||||
if (ext === '.mdx' || ext === '.md') {
|
||||
const parsed = matter(content);
|
||||
|
||||
return {
|
||||
type: 'page',
|
||||
path: virtualPath,
|
||||
data: {
|
||||
...parsed.data,
|
||||
content: parsed.content,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (ext === '.json') {
|
||||
return {
|
||||
type: 'meta',
|
||||
path: virtualPath,
|
||||
data: JSON.parse(content),
|
||||
};
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
import { create, docs } from '../content/docs';
|
||||
|
||||
export const source = loader({
|
||||
source: {
|
||||
files: virtualFiles,
|
||||
} as Source<{
|
||||
pageData: PageData & {
|
||||
content: string;
|
||||
};
|
||||
metaData: MetaData;
|
||||
}>,
|
||||
source: await create.sourceAsync(docs.doc, docs.meta),
|
||||
baseUrl: '/docs',
|
||||
});
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { fromConfig } from 'fumadocs-mdx/runtime/vite';
|
||||
import type * as Config from '../../source.config';
|
||||
|
||||
export const create = fromConfig<typeof Config>();
|
||||
|
||||
export const docs = create.docs('docs', {
|
||||
doc: import.meta.glob('/content/docs/**/*.mdx', {
|
||||
query: {
|
||||
collection: 'docs',
|
||||
},
|
||||
base: '/content/docs',
|
||||
}),
|
||||
meta: import.meta.glob('/content/docs/**/*.json', {
|
||||
query: {
|
||||
collection: 'docs',
|
||||
},
|
||||
base: '/content/docs',
|
||||
}),
|
||||
});
|
||||
@@ -9,17 +9,15 @@
|
||||
"typecheck": "react-router typegen && tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fumadocs/mdx-remote": "workspace:*",
|
||||
"@react-router/node": "^7.6.3",
|
||||
"@react-router/serve": "^7.6.3",
|
||||
"fumadocs-core": "workspace:*",
|
||||
"fumadocs-mdx": "workspace:*",
|
||||
"fumadocs-ui": "workspace:*",
|
||||
"gray-matter": "^4.0.3",
|
||||
"isbot": "^5.1.28",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router": "^7.6.3",
|
||||
"shiki": "^3.8.0"
|
||||
"react-router": "^7.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-router/dev": "^7.6.3",
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import type { Config } from '@react-router/dev/config';
|
||||
import { source } from './app/source';
|
||||
|
||||
export default {
|
||||
ssr: true,
|
||||
async prerender({ getStaticPaths }) {
|
||||
return [...getStaticPaths(), ...source.getPages().map((page) => page.url)];
|
||||
},
|
||||
} satisfies Config;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig, defineDocs } from 'fumadocs-mdx/config';
|
||||
|
||||
export const docs = defineDocs({
|
||||
dir: 'content/docs',
|
||||
});
|
||||
|
||||
export default defineConfig();
|
||||
@@ -2,12 +2,9 @@ import { reactRouter } from '@react-router/dev/vite';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import mdx from 'fumadocs-mdx/vite';
|
||||
import * as MdxConfig from './source.config';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
rollupOptions: {
|
||||
external: ['shiki'],
|
||||
},
|
||||
},
|
||||
plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
|
||||
plugins: [mdx(MdxConfig), tailwindcss(), reactRouter(), tsconfigPaths()],
|
||||
});
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
"import": "./dist/runtime/async.js",
|
||||
"types": "./dist/runtime/async.d.ts"
|
||||
},
|
||||
"./runtime/vite": {
|
||||
"import": "./dist/runtime/vite.js",
|
||||
"types": "./dist/runtime/vite.d.ts"
|
||||
},
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -68,6 +72,7 @@
|
||||
"zod": "^4.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"react": "^19.1.0",
|
||||
"@fumadocs/mdx-remote": "workspace:*",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/mdast": "^4.0.3",
|
||||
@@ -84,12 +89,16 @@
|
||||
"webpack": "^5.100.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"@fumadocs/mdx-remote": "^1.2.0",
|
||||
"fumadocs-core": "^14.0.0 || ^15.0.0",
|
||||
"next": "^15.3.0",
|
||||
"vite": "6.x.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"next": {
|
||||
"optional": true
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { GlobalConfig } from '@/config/types';
|
||||
import type { LoadedConfig } from '@/utils/config';
|
||||
import type {
|
||||
DocsCollection,
|
||||
DocCollection,
|
||||
DocsCollection,
|
||||
GlobalConfig,
|
||||
MetaCollection,
|
||||
} from '@/config/define';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type MDXOptions } from '@/utils/build-mdx';
|
||||
import { type GlobalConfig } from '@/config/types';
|
||||
import { frontmatterSchema, metaSchema } from '@/utils/schema';
|
||||
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
||||
import type { DefaultMDXOptions } from '@/utils/mdx-options';
|
||||
|
||||
export type CollectionSchema<Schema extends StandardSchemaV1, Context> =
|
||||
| Schema
|
||||
@@ -51,68 +51,53 @@ export interface DocsCollection<
|
||||
Async extends boolean = boolean,
|
||||
> {
|
||||
type: 'docs';
|
||||
/**
|
||||
* The directory to scan files
|
||||
*
|
||||
* @defaultValue 'content/docs'
|
||||
*/
|
||||
dir: string | string[];
|
||||
|
||||
docs: DocCollection<DocSchema, Async>;
|
||||
meta: MetaCollection<MetaSchema>;
|
||||
}
|
||||
|
||||
export function defineCollections<
|
||||
T extends 'doc' | 'meta',
|
||||
Schema extends StandardSchemaV1 = StandardSchemaV1<unknown, any>,
|
||||
Async extends boolean = false,
|
||||
>(
|
||||
options: { type: T } & (T extends 'doc'
|
||||
? DocCollection<Schema, Async>
|
||||
: MetaCollection<Schema>),
|
||||
): {
|
||||
type: T;
|
||||
export interface GlobalConfig {
|
||||
/**
|
||||
* Configure global MDX options
|
||||
*/
|
||||
mdxOptions?:
|
||||
| DefaultMDXOptions
|
||||
| (() => DefaultMDXOptions | Promise<DefaultMDXOptions>);
|
||||
|
||||
_type: {
|
||||
async: Async;
|
||||
schema: Schema;
|
||||
};
|
||||
} {
|
||||
return {
|
||||
// @ts-expect-error -- internal type inferring
|
||||
_type: undefined,
|
||||
...options,
|
||||
};
|
||||
/**
|
||||
* Fetch last modified time with specified version control
|
||||
* @defaultValue 'none'
|
||||
*/
|
||||
lastModifiedTime?: 'git' | 'none';
|
||||
}
|
||||
|
||||
export function defineCollections<
|
||||
Schema extends StandardSchemaV1 = StandardSchemaV1,
|
||||
Async extends boolean = false,
|
||||
>(options: DocCollection<Schema, Async>): DocCollection<Schema, Async>;
|
||||
export function defineCollections<
|
||||
Schema extends StandardSchemaV1 = StandardSchemaV1,
|
||||
>(options: MetaCollection<Schema>): MetaCollection<Schema>;
|
||||
|
||||
export function defineCollections(
|
||||
options: DocCollection | MetaCollection,
|
||||
): DocCollection | MetaCollection {
|
||||
return options as any;
|
||||
}
|
||||
|
||||
export function defineDocs<
|
||||
DocSchema extends StandardSchemaV1 = typeof frontmatterSchema,
|
||||
MetaSchema extends StandardSchemaV1 = typeof metaSchema,
|
||||
Async extends boolean = false,
|
||||
>(options?: {
|
||||
/**
|
||||
* The directory to scan files
|
||||
*
|
||||
* @defaultValue 'content/docs'
|
||||
*/
|
||||
dir?: string | string[];
|
||||
|
||||
docs?: Omit<DocCollection<DocSchema, Async>, 'dir' | 'type'>;
|
||||
meta?: Omit<MetaCollection<MetaSchema>, 'dir' | 'type'>;
|
||||
}): {
|
||||
type: 'docs';
|
||||
|
||||
docs: {
|
||||
type: 'doc';
|
||||
_type: {
|
||||
schema: DocSchema;
|
||||
async: Async;
|
||||
};
|
||||
};
|
||||
|
||||
meta: {
|
||||
type: 'meta';
|
||||
_type: {
|
||||
schema: MetaSchema;
|
||||
async: false;
|
||||
};
|
||||
};
|
||||
} {
|
||||
>(
|
||||
options?: Partial<DocsCollection<DocSchema, MetaSchema, Async>>,
|
||||
): DocsCollection<DocSchema, MetaSchema, Async> {
|
||||
if (!options)
|
||||
console.warn(
|
||||
'[`source.config.ts`] Deprecated: please pass options to `defineDocs()` and specify a `dir`.',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './types';
|
||||
export * from './define';
|
||||
export {
|
||||
loadDefaultOptions,
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
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';
|
||||
|
||||
export interface GlobalConfig {
|
||||
/**
|
||||
* Configure global MDX options
|
||||
*/
|
||||
mdxOptions?:
|
||||
| DefaultMDXOptions
|
||||
| (() => DefaultMDXOptions | Promise<DefaultMDXOptions>);
|
||||
|
||||
/**
|
||||
* Fetch last modified time with specified version control
|
||||
* @defaultValue 'none'
|
||||
*/
|
||||
lastModifiedTime?: 'git' | 'none';
|
||||
}
|
||||
|
||||
export interface FileInfo {
|
||||
path: string;
|
||||
absolutePath: string;
|
||||
}
|
||||
|
||||
export interface MarkdownProps {
|
||||
body: FC<MDXProps>;
|
||||
structuredData: StructuredData;
|
||||
toc: TableOfContents;
|
||||
_exports: Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Only available when `lastModifiedTime` is enabled on MDX loader
|
||||
*/
|
||||
lastModified?: Date;
|
||||
}
|
||||
|
||||
export interface BaseCollectionEntry {
|
||||
/**
|
||||
* Raw file path of collection entry, including absolute path (not normalized).
|
||||
*/
|
||||
_file: FileInfo;
|
||||
}
|
||||
@@ -2,12 +2,11 @@ import * as path from 'node:path';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { glob } from 'tinyglobby';
|
||||
import { getTypeFromPath } from '@/utils/get-type-from-path';
|
||||
import type { FileInfo } from '@/config/types';
|
||||
import type { LoadedConfig } from '@/utils/config';
|
||||
import type { DocCollection, MetaCollection } from '@/config';
|
||||
import { validate } from '@/utils/schema';
|
||||
import { fileCache } from '@/map/file-cache';
|
||||
import type { AsyncRuntimeFile } from '@/runtime/types';
|
||||
import type { AsyncRuntimeFile, FileInfo } from '@/runtime/types';
|
||||
import { load } from 'js-yaml';
|
||||
import { getGitTimestamp } from '@/utils/git-timestamp';
|
||||
import { fumaMatter } from '@/utils/fuma-matter';
|
||||
|
||||
@@ -4,8 +4,7 @@ import {
|
||||
type PageData,
|
||||
type Source,
|
||||
} from 'fumadocs-core/source';
|
||||
import { type BaseCollectionEntry, type FileInfo } from '@/config';
|
||||
import type { Runtime } from '@/runtime/types';
|
||||
import type { BaseCollectionEntry, FileInfo, Runtime } from '@/runtime/types';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const cache = new Map<string, string>();
|
||||
|
||||
@@ -1,7 +1,35 @@
|
||||
import type { BaseCollectionEntry, FileInfo, MarkdownProps } from '@/config';
|
||||
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
||||
import type { MetaData, PageData, Source } from 'fumadocs-core/source';
|
||||
import type { LoadedConfig } from '@/utils/config';
|
||||
import type { FC } from 'react';
|
||||
import type { MDXProps } from 'mdx/types';
|
||||
import type { StructuredData } from 'fumadocs-core/mdx-plugins';
|
||||
import type { TableOfContents } from 'fumadocs-core/server';
|
||||
import type { DocCollection, DocsCollection, MetaCollection } from '@/config';
|
||||
|
||||
export interface BaseCollectionEntry {
|
||||
/**
|
||||
* Raw file path of collection entry, including absolute path (not normalized).
|
||||
*/
|
||||
_file: FileInfo;
|
||||
}
|
||||
|
||||
export interface FileInfo {
|
||||
path: string;
|
||||
absolutePath: string;
|
||||
}
|
||||
|
||||
export interface MarkdownProps {
|
||||
body: FC<MDXProps>;
|
||||
structuredData: StructuredData;
|
||||
toc: TableOfContents;
|
||||
_exports: Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Only available when `lastModifiedTime` is enabled on MDX loader
|
||||
*/
|
||||
lastModified?: Date;
|
||||
}
|
||||
|
||||
export interface RuntimeFile {
|
||||
info: FileInfo;
|
||||
@@ -31,43 +59,16 @@ type MetaOut<Schema extends StandardSchemaV1> =
|
||||
StandardSchemaV1.InferOutput<Schema> & BaseCollectionEntry;
|
||||
|
||||
export interface Runtime {
|
||||
doc: <C>(files: RuntimeFile[]) => C extends {
|
||||
type: 'doc';
|
||||
_type: {
|
||||
schema: infer Schema extends StandardSchemaV1;
|
||||
};
|
||||
}
|
||||
? DocOut<Schema>[]
|
||||
: never;
|
||||
meta: <C>(files: RuntimeFile[]) => C extends {
|
||||
type: 'meta';
|
||||
|
||||
_type: {
|
||||
schema: infer Schema extends StandardSchemaV1;
|
||||
};
|
||||
}
|
||||
? MetaOut<Schema>[]
|
||||
: never;
|
||||
doc: <C>(
|
||||
files: RuntimeFile[],
|
||||
) => C extends DocCollection<infer Schema, false> ? DocOut<Schema>[] : never;
|
||||
meta: <C>(
|
||||
files: RuntimeFile[],
|
||||
) => C extends MetaCollection<infer Schema> ? MetaOut<Schema>[] : never;
|
||||
docs: <C>(
|
||||
docs: RuntimeFile[],
|
||||
metas: RuntimeFile[],
|
||||
) => C extends {
|
||||
type: 'docs';
|
||||
|
||||
docs: {
|
||||
type: 'doc';
|
||||
_type: {
|
||||
schema: infer DocSchema extends StandardSchemaV1;
|
||||
};
|
||||
};
|
||||
|
||||
meta: {
|
||||
type: 'meta';
|
||||
_type: {
|
||||
schema: infer MetaSchema extends StandardSchemaV1;
|
||||
};
|
||||
};
|
||||
}
|
||||
) => C extends DocsCollection<infer DocSchema, infer MetaSchema, false>
|
||||
? {
|
||||
docs: DocOut<DocSchema>[];
|
||||
meta: MetaOut<MetaSchema>[];
|
||||
@@ -96,12 +97,7 @@ export interface RuntimeAsync {
|
||||
files: AsyncRuntimeFile[],
|
||||
collection: string,
|
||||
config: LoadedConfig,
|
||||
) => C extends {
|
||||
type: 'doc';
|
||||
_type: {
|
||||
schema: infer Schema extends StandardSchemaV1;
|
||||
};
|
||||
}
|
||||
) => C extends DocCollection<infer Schema, true>
|
||||
? AsyncDocOut<Schema>[]
|
||||
: never;
|
||||
docs: <C>(
|
||||
@@ -109,23 +105,7 @@ export interface RuntimeAsync {
|
||||
metas: RuntimeFile[],
|
||||
collection: string,
|
||||
config: LoadedConfig,
|
||||
) => C extends {
|
||||
type: 'docs';
|
||||
|
||||
docs: {
|
||||
type: 'doc';
|
||||
_type: {
|
||||
schema: infer DocSchema extends StandardSchemaV1;
|
||||
};
|
||||
};
|
||||
|
||||
meta: {
|
||||
type: 'meta';
|
||||
_type: {
|
||||
schema: infer MetaSchema extends StandardSchemaV1;
|
||||
};
|
||||
};
|
||||
}
|
||||
) => C extends DocsCollection<infer DocSchema, infer MetaSchema, true>
|
||||
? {
|
||||
docs: AsyncDocOut<DocSchema>[];
|
||||
meta: MetaOut<MetaSchema>[];
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
import type { TableOfContents } from 'fumadocs-core/server';
|
||||
import { type FC, lazy, type LazyExoticComponent, type ReactNode } from 'react';
|
||||
import type { MDXProps } from 'mdx/types';
|
||||
import type { StructuredData } from 'fumadocs-core/mdx-plugins';
|
||||
import type { DocCollection, DocsCollection, MetaCollection } from '@/config';
|
||||
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
||||
import type {
|
||||
MetaData,
|
||||
PageData,
|
||||
Source,
|
||||
VirtualFile,
|
||||
} from 'fumadocs-core/source';
|
||||
|
||||
export type CompiledMDXFile<Frontmatter> = {
|
||||
frontmatter: Frontmatter;
|
||||
toc: TableOfContents;
|
||||
default: FC<MDXProps>;
|
||||
structuredData: StructuredData;
|
||||
} & Record<string, unknown>;
|
||||
|
||||
type MDXFileToPageData<Frontmatter> = Frontmatter & {
|
||||
toc: TableOfContents;
|
||||
default: FC<MDXProps>;
|
||||
structuredData: StructuredData;
|
||||
_exports: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type AttachGlobValue<GlobValue, Attach> =
|
||||
GlobValue extends () => Promise<unknown> ? () => Promise<Attach> : Attach;
|
||||
|
||||
export function fromConfig<Config>(): {
|
||||
doc: <Name extends keyof Config, GlobValue>(
|
||||
name: Name,
|
||||
glob: Record<string, GlobValue>,
|
||||
) => Config[Name] extends DocCollection<infer Schema>
|
||||
? Record<
|
||||
string,
|
||||
AttachGlobValue<
|
||||
GlobValue,
|
||||
CompiledMDXFile<StandardSchemaV1.InferOutput<Schema>>
|
||||
>
|
||||
>
|
||||
: never;
|
||||
|
||||
meta: <Name extends keyof Config, GlobValue>(
|
||||
name: Name,
|
||||
glob: Record<string, GlobValue>,
|
||||
) => Config[Name] extends MetaCollection<infer Schema>
|
||||
? AttachGlobValue<GlobValue, StandardSchemaV1.InferOutput<Schema>>
|
||||
: never;
|
||||
|
||||
docs: <Name extends keyof Config, DocGlobValue, MetaGlobValue>(
|
||||
name: Name,
|
||||
options: {
|
||||
meta: Record<string, MetaGlobValue>;
|
||||
doc: Record<string, DocGlobValue>;
|
||||
},
|
||||
) => Config[Name] extends DocsCollection<infer DocSchema, infer MetaSchema>
|
||||
? {
|
||||
doc: Record<
|
||||
string,
|
||||
AttachGlobValue<
|
||||
DocGlobValue,
|
||||
CompiledMDXFile<StandardSchemaV1.InferOutput<DocSchema>>
|
||||
>
|
||||
>;
|
||||
meta: Record<
|
||||
string,
|
||||
AttachGlobValue<
|
||||
MetaGlobValue,
|
||||
StandardSchemaV1.InferOutput<MetaSchema>
|
||||
>
|
||||
>;
|
||||
}
|
||||
: never;
|
||||
source: <DocOut extends PageData, MetaOut extends MetaData>(
|
||||
doc: Record<string, CompiledMDXFile<DocOut>>,
|
||||
meta: Record<string, MetaOut>,
|
||||
) => Source<{
|
||||
pageData: MDXFileToPageData<DocOut>;
|
||||
metaData: MetaOut;
|
||||
}>;
|
||||
|
||||
sourceAsync: <DocOut extends PageData, MetaOut extends MetaData>(
|
||||
doc: Record<string, () => Promise<CompiledMDXFile<DocOut>>>,
|
||||
meta: Record<string, () => Promise<MetaOut>>,
|
||||
) => Promise<
|
||||
Source<{
|
||||
pageData: MDXFileToPageData<DocOut>;
|
||||
metaData: MetaOut;
|
||||
}>
|
||||
>;
|
||||
} {
|
||||
function normalize(entries: Record<string, unknown>) {
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const k in entries) {
|
||||
const mappedK = k.startsWith('./') ? k.slice(2) : k;
|
||||
out[mappedK] = entries[k];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function mapPageData<Frontmatter>(
|
||||
entry: CompiledMDXFile<Frontmatter>,
|
||||
): MDXFileToPageData<Frontmatter> {
|
||||
const { toc, structuredData } = entry;
|
||||
|
||||
return {
|
||||
...entry.frontmatter,
|
||||
default: entry.default,
|
||||
toc,
|
||||
structuredData,
|
||||
_exports: entry,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
doc(_, glob) {
|
||||
return normalize(glob) as any;
|
||||
},
|
||||
meta(_, glob) {
|
||||
return normalize(glob) as any;
|
||||
},
|
||||
docs(_, { doc, meta }) {
|
||||
return {
|
||||
doc: normalize(doc),
|
||||
meta: normalize(meta),
|
||||
} as any;
|
||||
},
|
||||
source(doc, meta) {
|
||||
const virtualFiles: VirtualFile[] = [];
|
||||
for (const [file, content] of Object.entries(doc)) {
|
||||
virtualFiles.push({
|
||||
type: 'page',
|
||||
path: file,
|
||||
data: mapPageData(content),
|
||||
});
|
||||
}
|
||||
|
||||
for (const [file, content] of Object.entries(meta)) {
|
||||
virtualFiles.push({
|
||||
type: 'meta',
|
||||
path: file,
|
||||
data: content,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
files: virtualFiles,
|
||||
};
|
||||
},
|
||||
async sourceAsync(doc, meta) {
|
||||
const virtualFiles: VirtualFile[] = [];
|
||||
|
||||
for (const [file, content] of Object.entries(doc)) {
|
||||
virtualFiles.push({
|
||||
type: 'page',
|
||||
path: file,
|
||||
data: mapPageData(await content()),
|
||||
});
|
||||
}
|
||||
|
||||
for (const [file, content] of Object.entries(meta)) {
|
||||
virtualFiles.push({
|
||||
type: 'meta',
|
||||
path: file,
|
||||
data: await content(),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
files: virtualFiles,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type ClientLoader<Props> = Record<
|
||||
string,
|
||||
LazyExoticComponent<(props: Props) => ReactNode>
|
||||
>;
|
||||
|
||||
export function toClientRenderer<Frontmatter, Props extends object = object>(
|
||||
files: Record<string, () => Promise<CompiledMDXFile<Frontmatter>>>,
|
||||
renderer: (loaded: CompiledMDXFile<Frontmatter>, props: Props) => ReactNode,
|
||||
): ClientLoader<Props> {
|
||||
const loader: ClientLoader<Props> = {};
|
||||
|
||||
for (const k in files) {
|
||||
loader[k] = lazy(async () => {
|
||||
const loaded = await files[k]();
|
||||
|
||||
return { default: (props) => renderer(loaded, props) };
|
||||
});
|
||||
}
|
||||
|
||||
return loader;
|
||||
}
|
||||
@@ -3,9 +3,9 @@ import * as path from 'node:path';
|
||||
import type {
|
||||
DocCollection,
|
||||
DocsCollection,
|
||||
GlobalConfig,
|
||||
MetaCollection,
|
||||
} from '@/config/define';
|
||||
import { type GlobalConfig } from '@/config/types';
|
||||
import type { ProcessorOptions } from '@mdx-js/mdx';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { buildConfig } from '@/config/build';
|
||||
|
||||
@@ -4,18 +4,14 @@ import { buildMDX } from '@/utils/build-mdx';
|
||||
import { parse } from 'node:querystring';
|
||||
import { countLines } from '@/utils/count-lines';
|
||||
import { fumaMatter } from '@/utils/fuma-matter';
|
||||
import { loadDefaultOptions } from '@/utils/mdx-options';
|
||||
|
||||
export interface CreateMDXOptions {
|
||||
configPath?: string;
|
||||
}
|
||||
import { validate, ValidationError } from '@/utils/schema';
|
||||
import { z } from 'zod';
|
||||
|
||||
const fileRegex = /\.(md|mdx)$/;
|
||||
|
||||
export default function unstable_mdx(
|
||||
config: Record<string, unknown>,
|
||||
_options: CreateMDXOptions = {},
|
||||
): Plugin {
|
||||
const onlySchema = z.literal(['frontmatter', 'all']);
|
||||
|
||||
export default function mdx(config: Record<string, unknown>): Plugin {
|
||||
const [err, loaded] = buildConfig(config);
|
||||
if (err || !loaded) {
|
||||
throw new Error(err);
|
||||
@@ -23,6 +19,8 @@ export default function unstable_mdx(
|
||||
|
||||
return {
|
||||
name: 'fumadocs-mdx',
|
||||
// needed, otherwise other plugins will be executed before our `transform`.
|
||||
enforce: 'pre',
|
||||
// TODO: need a way to generate .source folder that works for non-RSC based frameworks, currently, we need to dynamic import MDX files using `import.meta.glob`.
|
||||
// at the moment, RR and Tanstack Start has no stable support for RSC yet.
|
||||
|
||||
@@ -32,35 +30,64 @@ export default function unstable_mdx(
|
||||
|
||||
const matter = fumaMatter(value);
|
||||
const isDevelopment = this.environment.mode === 'dev';
|
||||
const { collection: collectionId, raw } = parse(query) as {
|
||||
const parsed = parse(query) as {
|
||||
collection?: string;
|
||||
raw?: string;
|
||||
only?: string;
|
||||
};
|
||||
|
||||
const collection = collectionId
|
||||
? loaded.collections.get(collectionId)
|
||||
const collection = parsed.collection
|
||||
? loaded.collections.get(parsed.collection)
|
||||
: undefined;
|
||||
const only = parsed.only ? onlySchema.parse(parsed.only) : 'all';
|
||||
|
||||
// ensure the line number is correct in dev mode
|
||||
const lineOffset = '\n'.repeat(
|
||||
isDevelopment ? countLines(matter.matter) : 0,
|
||||
);
|
||||
|
||||
let schema;
|
||||
let mdxOptions;
|
||||
switch (collection?.type) {
|
||||
case 'doc':
|
||||
mdxOptions = collection.mdxOptions;
|
||||
schema = collection.schema;
|
||||
break;
|
||||
case 'docs':
|
||||
mdxOptions = collection.docs.mdxOptions;
|
||||
schema = collection.docs.schema;
|
||||
break;
|
||||
}
|
||||
|
||||
if (schema) {
|
||||
try {
|
||||
matter.data = await validate(
|
||||
schema,
|
||||
matter.data,
|
||||
{
|
||||
source: value,
|
||||
path,
|
||||
},
|
||||
`invalid frontmatter in ${path}`,
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof ValidationError) {
|
||||
throw new Error(e.toStringFormatted());
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (only === 'frontmatter') {
|
||||
return {
|
||||
code: `export const frontmatter = ${JSON.stringify(matter.data)}`,
|
||||
};
|
||||
}
|
||||
|
||||
const { loadDefaultOptions } = await import('@/utils/mdx-options');
|
||||
mdxOptions ??= await loadDefaultOptions(loaded);
|
||||
|
||||
// ensure the line number is correct in dev mode
|
||||
const lineOffset = isDevelopment ? countLines(matter.matter) : 0;
|
||||
|
||||
const file = await buildMDX(
|
||||
collectionId ?? 'global',
|
||||
lineOffset + matter.content,
|
||||
parsed.collection ?? 'global',
|
||||
'\n'.repeat(lineOffset) + matter.content,
|
||||
{
|
||||
development: isDevelopment,
|
||||
...mdxOptions,
|
||||
@@ -75,10 +102,8 @@ export default function unstable_mdx(
|
||||
);
|
||||
|
||||
return {
|
||||
code:
|
||||
typeof raw === 'string'
|
||||
? `export default ${JSON.stringify(file.value)}`
|
||||
: String(file.value),
|
||||
code: String(file.value),
|
||||
map: file.map,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -63,10 +63,10 @@ for (const { name, collection } of cases) {
|
||||
const out = await generateJS(
|
||||
path.join(file, './fixtures/config.ts'),
|
||||
{
|
||||
// @ts-expect-error -- test file
|
||||
_runtime: {
|
||||
files: new Map(),
|
||||
},
|
||||
// @ts-expect-error -- test file
|
||||
collections: new Map([['docs', collection]]),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ export default defineConfig({
|
||||
entry: [
|
||||
'./src/{index,loader-mdx}.ts',
|
||||
'./src/{config,next,vite}/index.ts',
|
||||
'./src/runtime/async.ts',
|
||||
'./src/runtime/{async,vite}.ts',
|
||||
],
|
||||
format: ['esm', 'cjs'],
|
||||
external: ['next', 'typescript'],
|
||||
|
||||
Generated
+9
-12
@@ -279,7 +279,7 @@ importers:
|
||||
version: 0.2.2(@content-collections/[email protected]([email protected]))([email protected])([email protected]([email protected]))([email protected])
|
||||
'@content-collections/next':
|
||||
specifier: ^0.2.6
|
||||
version: 0.2.6(@content-collections/[email protected]([email protected]))([email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))
|
||||
version: 0.2.6(@content-collections/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))
|
||||
'@types/mdx':
|
||||
specifier: ^2.0.13
|
||||
version: 2.0.13
|
||||
@@ -476,9 +476,6 @@ importers:
|
||||
|
||||
examples/react-router:
|
||||
dependencies:
|
||||
'@fumadocs/mdx-remote':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/mdx-remote
|
||||
'@react-router/node':
|
||||
specifier: ^7.6.3
|
||||
version: 7.6.3([email protected]([email protected]([email protected]))([email protected]))([email protected])
|
||||
@@ -488,12 +485,12 @@ importers:
|
||||
fumadocs-core:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/core
|
||||
fumadocs-mdx:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/mdx
|
||||
fumadocs-ui:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ui
|
||||
gray-matter:
|
||||
specifier: ^4.0.3
|
||||
version: 4.0.3
|
||||
isbot:
|
||||
specifier: ^5.1.28
|
||||
version: 5.1.28
|
||||
@@ -506,9 +503,6 @@ importers:
|
||||
react-router:
|
||||
specifier: ^7.6.3
|
||||
version: 7.6.3([email protected]([email protected]))([email protected])
|
||||
shiki:
|
||||
specifier: ^3.8.0
|
||||
version: 3.8.0
|
||||
devDependencies:
|
||||
'@react-router/dev':
|
||||
specifier: ^7.6.3
|
||||
@@ -932,7 +926,7 @@ importers:
|
||||
version: 0.2.2(@content-collections/[email protected]([email protected]))([email protected])([email protected]([email protected]))([email protected])
|
||||
'@content-collections/next':
|
||||
specifier: ^0.2.6
|
||||
version: 0.2.6(@content-collections/[email protected]([email protected]))([email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))
|
||||
version: 0.2.6(@content-collections/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))
|
||||
'@react-router/dev':
|
||||
specifier: ^7.6.3
|
||||
version: 7.6.3(@react-router/[email protected]([email protected]([email protected]([email protected]))([email protected]))([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected])([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))([email protected])
|
||||
@@ -1141,6 +1135,9 @@ importers:
|
||||
next:
|
||||
specifier: ^15.4.1
|
||||
version: 15.4.1(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
react:
|
||||
specifier: ^19.1.0
|
||||
version: 19.1.0
|
||||
tsconfig:
|
||||
specifier: workspace:*
|
||||
version: link:../tsconfig
|
||||
@@ -10556,7 +10553,7 @@ snapshots:
|
||||
- acorn
|
||||
- supports-color
|
||||
|
||||
'@content-collections/[email protected](@content-collections/[email protected]([email protected]))([email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))':
|
||||
'@content-collections/[email protected](@content-collections/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))':
|
||||
dependencies:
|
||||
'@content-collections/core': 0.10.0([email protected])
|
||||
'@content-collections/integrations': 0.2.1(@content-collections/[email protected]([email protected]))
|
||||
|
||||
Reference in New Issue
Block a user