MDX: Add useContent API to client loader for avoiding Lint errors

This commit is contained in:
Fuma Nama
2025-10-12 13:14:57 +08:00
parent cbc93e9902
commit 1f1c7877e7
9 changed files with 170 additions and 178 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-mdx': patch
---
Add `useContent` API to client loader for avoiding Lint errors
+1 -1
View File
@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build && tsc --noEmit",
"build": "vite build",
"start": "node .output/server/index.mjs"
},
"dependencies": {
-6
View File
@@ -10,9 +10,3 @@ export function getRouter() {
defaultNotFoundComponent: NotFound,
});
}
declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof getRouter>;
}
}
+2 -3
View File
@@ -1,7 +1,6 @@
import type { StructuredData } from 'fumadocs-core/mdx-plugins';
import type { TOCItemType } from 'fumadocs-core/toc';
import type { FC } from 'react';
import type { MDXProps } from 'mdx/types';
import type { MDXContent } from 'mdx/types';
import type { ExtractedReference } from '@/loaders/mdx/remark-postprocess';
export interface FileInfo {
@@ -20,7 +19,7 @@ export interface DocData {
/**
* Compiled MDX content (as component)
*/
body: FC<MDXProps>;
body: MDXContent;
/**
* table of contents generated from content.
+113 -1
View File
@@ -1,6 +1,26 @@
import type { DocCollection, DocsCollection, MetaCollection } from '@/config';
import type { StandardSchemaV1 } from '@standard-schema/spec';
import type { DocMap, LazyDocMap, MetaMap } from '@/runtime/vite/types';
import type { CompiledMDXProperties } from '@/loaders/mdx/build-mdx';
import { createElement, type FC, lazy, type ReactNode } from 'react';
export type CompiledMDXFile<Frontmatter> = CompiledMDXProperties<Frontmatter> &
Record<string, unknown>;
export type DocMap<Frontmatter> = Record<
string,
(() => Promise<CompiledMDXFile<Frontmatter>>) & { base: string }
>;
export type MetaMap<Data> = Record<
string,
(() => Promise<Data>) & { base: string }
>;
export interface LazyDocMap<Frontmatter> {
base: string;
head: Record<string, () => Promise<Frontmatter>>;
body: Record<string, () => Promise<CompiledMDXFile<Frontmatter>>>;
}
export interface BaseCreate<Config> {
doc: <Name extends keyof Config>(
@@ -65,3 +85,95 @@ export function fromConfigBase<Config>(): BaseCreate<Config> {
},
};
}
export interface ClientLoaderOptions<Frontmatter, Props> {
/**
* Loader ID (usually your collection name)
*
* The code splitting strategy of frameworks like Tanstack Start may duplicate `createClientLoader()` into different chunks.
*
* We use loader ID to share cache between multiple instances of client loader.
*
* @defaultValue ''
*/
id?: string;
component: (loaded: CompiledMDXFile<Frontmatter>, props: Props) => ReactNode;
}
export interface ClientLoader<Frontmatter, Props> {
preload: (path: string) => Promise<CompiledMDXFile<Frontmatter>>;
/**
* Get a component that renders content with `React.lazy`.
*/
getComponent: (path: string) => FC<Props>;
/**
* Get react nodes that renders content with `React.lazy`.
*/
useContent: (path: string, props: Props) => ReactNode;
getRenderer: () => Record<string, FC<Props>>;
}
const loaderStore = new Map<
string,
{
preloaded: Map<string, CompiledMDXFile<any>>;
}
>();
export function createClientLoader<Frontmatter, Props extends object = object>(
files: Record<string, () => Promise<CompiledMDXFile<Frontmatter>>>,
options: ClientLoaderOptions<Frontmatter, Props>,
): ClientLoader<Frontmatter, Props> {
const { id = '', component } = options;
let renderer: Record<string, FC<Props>> | undefined;
const store = loaderStore.get(id) ?? {
preloaded: new Map(),
};
loaderStore.set(id, store);
function getRenderer() {
if (renderer) return renderer;
renderer = {};
for (const k in files) {
const OnDemand = lazy(async () => {
const loaded = await files[k]();
return { default: (props) => component(loaded, props) };
});
renderer[k] = (props: Props) => {
const cached = store.preloaded.get(k);
if (!cached) return createElement(OnDemand, props);
return component(cached, props);
};
}
return renderer;
}
return {
async preload(path) {
const loaded = await files[path]();
store.preloaded.set(path, loaded);
return loaded;
},
getRenderer,
getComponent(path) {
return getRenderer()[path];
},
useContent(path, props) {
const Comp = this.getComponent(path);
return createElement(Comp, props);
},
};
}
export function toClientRenderer<Frontmatter, Props extends object = object>(
files: Record<string, () => Promise<CompiledMDXFile<Frontmatter>>>,
component: (loaded: CompiledMDXFile<Frontmatter>, props: Props) => ReactNode,
): Record<string, FC<Props>> {
return createClientLoader(files, { component }).getRenderer();
}
+1 -88
View File
@@ -1,89 +1,2 @@
import type { CompiledMDXFile } from '@/runtime/vite/types';
import { createElement, type FC, lazy, type ReactNode } from 'react';
export * from './base';
export { fromConfigBase as fromConfig } from './base';
export interface ClientLoaderOptions<Frontmatter, Props> {
/**
* Loader ID (usually your collection name)
*
* The code splitting strategy of frameworks like Tanstack Start may duplicate `createClientLoader()` into different chunks.
*
* We use loader ID to share cache between multiple instances of client loader.
*
* @defaultValue ''
*/
id?: string;
component: (loaded: CompiledMDXFile<Frontmatter>, props: Props) => ReactNode;
}
type ClientRenderer<Props> = Record<string, FC<Props>>;
export interface ClientLoader<Frontmatter, Props> {
preload: (path: string) => Promise<CompiledMDXFile<Frontmatter>>;
/**
* Get a component that renders content with `React.lazy`
*/
getComponent: (path: string) => FC<Props>;
getRenderer: () => ClientRenderer<Props>;
}
const loaderStore = new Map<
string,
{
preloaded: Map<string, CompiledMDXFile<any>>;
}
>();
export function createClientLoader<Frontmatter, Props = object>(
files: Record<string, () => Promise<CompiledMDXFile<Frontmatter>>>,
options: ClientLoaderOptions<Frontmatter, Props>,
): ClientLoader<Frontmatter, Props> {
const { id = '', component } = options;
let renderer: ClientRenderer<Props> | undefined;
const store = loaderStore.get(id) ?? {
preloaded: new Map(),
};
loaderStore.set(id, store);
function getRenderer() {
if (renderer) return renderer;
renderer = {};
for (const k in files) {
const OnDemand = lazy(async () => {
const loaded = await files[k]();
return { default: (props) => component(loaded, props) };
});
renderer[k] = (props: Props) => {
const cached = store.preloaded.get(k);
if (!cached) return createElement(OnDemand, props);
return component(cached, props);
};
}
return renderer;
}
return {
async preload(path) {
const loaded = await files[path]();
store.preloaded.set(path, loaded);
return loaded;
},
getRenderer,
getComponent(path) {
return getRenderer()[path];
},
};
}
export function toClientRenderer<Frontmatter, Props = object>(
files: Record<string, () => Promise<CompiledMDXFile<Frontmatter>>>,
component: (loaded: CompiledMDXFile<Frontmatter>, props: Props) => ReactNode,
): ClientRenderer<Props> {
return createClientLoader(files, { component }).getRenderer();
}
+9 -7
View File
@@ -1,11 +1,17 @@
import type { CompiledMDXFile, DocMap, LazyDocMap, MetaMap } from './types';
import type {
MetaData,
PageData,
Source,
VirtualFile,
} from 'fumadocs-core/source';
import { type BaseCreate, fromConfigBase } from '@/runtime/vite/base';
import {
type BaseCreate,
type CompiledMDXFile,
type DocMap,
fromConfigBase,
type LazyDocMap,
type MetaMap,
} from '@/runtime/vite/base';
import * as path from 'node:path';
import {
type AsyncDocCollectionEntry,
@@ -17,11 +23,7 @@ import {
} from '@/runtime/shared';
import fs from 'node:fs/promises';
// for server-side usage of renderers
export { createClientLoader, toClientRenderer } from './browser';
export type { ClientLoader, ClientLoaderOptions } from './browser';
export type * from './types';
export * from './base';
export interface ServerCreate<Config> extends BaseCreate<Config> {
sourceAsync: <DocOut extends PageData, MetaOut extends MetaData>(
-20
View File
@@ -1,20 +0,0 @@
import type { CompiledMDXProperties } from '@/loaders/mdx/build-mdx';
export type CompiledMDXFile<Frontmatter> = CompiledMDXProperties<Frontmatter> &
Record<string, unknown>;
export type DocMap<Frontmatter> = Record<
string,
(() => Promise<CompiledMDXFile<Frontmatter>>) & { base: string }
>;
export type MetaMap<Data> = Record<
string,
(() => Promise<Data>) & { base: string }
>;
export interface LazyDocMap<Frontmatter> {
base: string;
head: Record<string, () => Promise<Frontmatter>>;
body: Record<string, () => Promise<CompiledMDXFile<Frontmatter>>>;
}
+39 -52
View File
@@ -847,7 +847,7 @@ importers:
version: 1.132.51(@tanstack/[email protected]([email protected]([email protected]))([email protected]))(@tanstack/[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])
'@tanstack/react-start':
specifier: ^1.132.51
version: 1.132.51([email protected]([email protected]))([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))([email protected](@swc/[email protected])([email protected]))
version: 1.132.51([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](@swc/[email protected])([email protected]))
fumadocs-core:
specifier: workspace:*
version: link:../../packages/core
@@ -914,7 +914,7 @@ importers:
version: 1.132.51(@tanstack/[email protected]([email protected]([email protected]))([email protected]))(@tanstack/[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])
'@tanstack/react-start':
specifier: ^1.132.51
version: 1.132.51([email protected]([email protected]))([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))([email protected](@swc/[email protected])([email protected]))
version: 1.132.51([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](@swc/[email protected])([email protected]))
fumadocs-core:
specifier: workspace:*
version: link:../../packages/core
@@ -1401,7 +1401,7 @@ importers:
version: 1.132.47([email protected]([email protected]))([email protected])
'@tanstack/react-start':
specifier: ^1.132.51
version: 1.132.51([email protected]([email protected]))([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))([email protected](@swc/[email protected])([email protected]))
version: 1.132.51([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](@swc/[email protected])([email protected]))
'@types/mdx':
specifier: ^2.0.13
version: 2.0.13
@@ -5958,10 +5958,6 @@ packages:
[email protected]:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
[email protected]:
resolution: {integrity: sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA==}
hasBin: true
[email protected]:
resolution: {integrity: sha512-qsJ8/X+UypqxHXN75M7dF88jNK37dLBRW7LeUzCPz+TNs37G8cfWy9nWzS+LS//g600zrt2le9KuXt0rWfDz5Q==}
hasBin: true
@@ -6049,6 +6045,7 @@ packages:
[email protected]:
resolution: {integrity: sha512-7dXwfbuEpenPMQlwKb9rDLo22OfCEYCQjvkWRQz8IkWzEd5zRk/goMXtEGnNeK9FqxBfLXOYolEro8IJ1JQh9w==}
cpu: [arm64, x64]
os: [darwin, linux, win32]
hasBin: true
@@ -6412,6 +6409,14 @@ packages:
[email protected]:
resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==}
[email protected]:
resolution: {integrity: sha512-E7WKBcHVhAVrY6JYD5kteNqVq1GSZxqGrdSiwXR9at+XHi43HJoCQKXcCczR5LBnBquFZPsB3o7HklulKoBU5w==}
peerDependencies:
srvx: '>=0.7.1'
peerDependenciesMeta:
srvx:
optional: true
[email protected]:
resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}
@@ -6747,10 +6752,6 @@ packages:
engines: {node: '>=0.10'}
hasBin: true
[email protected]:
resolution: {integrity: sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==}
engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@@ -7377,9 +7378,6 @@ packages:
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
engines: {node: '>= 0.4'}
[email protected]:
resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
[email protected]:
resolution: {integrity: sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==}
@@ -9782,11 +9780,6 @@ packages:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
[email protected]:
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
engines: {node: '>=10'}
hasBin: true
[email protected]:
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
engines: {node: '>=10'}
@@ -11603,7 +11596,7 @@ snapshots:
outdent: 0.5.0
prettier: 2.8.8
resolve-from: 5.0.0
semver: 7.7.2
semver: 7.7.3
'@changesets/[email protected]':
dependencies:
@@ -11612,7 +11605,7 @@ snapshots:
'@changesets/should-skip-package': 0.1.2
'@changesets/types': 6.1.0
'@manypkg/get-packages': 1.1.3
semver: 7.7.2
semver: 7.7.3
'@changesets/[email protected]':
dependencies:
@@ -11645,7 +11638,7 @@ snapshots:
package-manager-detector: 0.2.11
picocolors: 1.1.1
resolve-from: 5.0.0
semver: 7.7.2
semver: 7.7.3
spawndamnit: 3.0.1
term-size: 2.2.1
transitivePeerDependencies:
@@ -11670,7 +11663,7 @@ snapshots:
'@changesets/types': 6.1.0
'@manypkg/get-packages': 1.1.3
picocolors: 1.1.1
semver: 7.7.2
semver: 7.7.3
'@changesets/[email protected]':
dependencies:
@@ -12505,7 +12498,7 @@ snapshots:
'@mapbox/[email protected]':
dependencies:
consola: 3.4.2
detect-libc: 2.1.1
detect-libc: 2.1.2
https-proxy-agent: 7.0.6
node-fetch: 2.7.0
nopt: 8.1.0
@@ -14297,7 +14290,7 @@ snapshots:
'@tailwindcss/[email protected]':
dependencies:
detect-libc: 2.1.1
detect-libc: 2.1.2
tar: 7.5.1
optionalDependencies:
'@tailwindcss/oxide-android-arm64': 4.1.14
@@ -14431,27 +14424,27 @@ snapshots:
tiny-invariant: 1.3.3
tiny-warning: 1.0.3
'@tanstack/[email protected]([email protected]([email protected]))([email protected])':
'@tanstack/[email protected]([email protected])([email protected]([email protected]))([email protected])':
dependencies:
'@tanstack/history': 1.132.31
'@tanstack/react-router': 1.132.47([email protected]([email protected]))([email protected])
'@tanstack/router-core': 1.132.47
'@tanstack/start-client-core': 1.132.48
'@tanstack/start-server-core': 1.132.48
'@tanstack/start-server-core': 1.132.48([email protected])
react: 19.2.0
react-dom: 19.2.0([email protected])
transitivePeerDependencies:
- crossws
'@tanstack/[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](@swc/[email protected])([email protected]))':
'@tanstack/[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](@swc/[email protected])([email protected]))':
dependencies:
'@tanstack/react-router': 1.132.47([email protected]([email protected]))([email protected])
'@tanstack/react-start-client': 1.132.48([email protected]([email protected]))([email protected])
'@tanstack/react-start-server': 1.132.48([email protected]([email protected]))([email protected])
'@tanstack/react-start-server': 1.132.48([email protected])([email protected]([email protected]))([email protected])
'@tanstack/router-utils': 1.132.51
'@tanstack/start-client-core': 1.132.48
'@tanstack/start-plugin-core': 1.132.51(@tanstack/[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](@swc/[email protected])([email protected]))
'@tanstack/start-server-core': 1.132.48
'@tanstack/start-plugin-core': 1.132.51(@tanstack/[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](@swc/[email protected])([email protected]))
'@tanstack/start-server-core': 1.132.48([email protected])
pathe: 2.0.3
react: 19.2.0
react-dom: 19.2.0([email protected])
@@ -14576,7 +14569,7 @@ snapshots:
tiny-invariant: 1.3.3
tiny-warning: 1.0.3
'@tanstack/[email protected](@tanstack/[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](@swc/[email protected])([email protected]))':
'@tanstack/[email protected](@tanstack/[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](@swc/[email protected])([email protected]))':
dependencies:
'@babel/code-frame': 7.26.2
'@babel/core': 7.28.4
@@ -14588,7 +14581,7 @@ snapshots:
'@tanstack/router-utils': 1.132.51
'@tanstack/server-functions-plugin': 1.132.51([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))
'@tanstack/start-client-core': 1.132.48
'@tanstack/start-server-core': 1.132.48
'@tanstack/start-server-core': 1.132.48([email protected])
babel-dead-code-elimination: 1.0.10
cheerio: 1.1.2
exsolve: 1.0.7
@@ -14608,13 +14601,13 @@ snapshots:
- vite-plugin-solid
- webpack
'@tanstack/[email protected]':
'@tanstack/[email protected]([email protected])':
dependencies:
'@tanstack/history': 1.132.31
'@tanstack/router-core': 1.132.47
'@tanstack/start-client-core': 1.132.48
'@tanstack/start-storage-context': 1.132.48
h3-v2: [email protected]
h3-v2: [email protected]([email protected])
seroval: 1.3.2
tiny-invariant: 1.3.3
transitivePeerDependencies:
@@ -15623,8 +15616,6 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]:
@@ -15719,7 +15710,7 @@ snapshots:
[email protected]:
dependencies:
baseline-browser-mapping: 2.8.10
baseline-browser-mapping: 2.8.15
[email protected]:
dependencies:
@@ -16121,6 +16112,9 @@ snapshots:
dependencies:
uncrypto: 0.1.3
[email protected]:
optional: true
[email protected]:
dependencies:
boolbase: 1.0.0
@@ -16435,10 +16429,7 @@ snapshots:
[email protected]: {}
[email protected].1: {}
[email protected]:
optional: true
[email protected].2: {}
[email protected]: {}
@@ -16517,7 +16508,7 @@ snapshots:
'@one-ini/wasm': 0.1.1
commander: 10.0.1
minimatch: 9.0.1
semver: 7.7.2
semver: 7.7.3
[email protected]: {}
@@ -17280,10 +17271,6 @@ snapshots:
es-errors: 1.3.0
get-intrinsic: 1.3.0
[email protected]:
dependencies:
resolve-pkg-maps: 1.0.0
[email protected]:
dependencies:
resolve-pkg-maps: 1.0.0
@@ -17407,12 +17394,14 @@ snapshots:
ufo: 1.6.1
uncrypto: 0.1.3
[email protected]:
[email protected]([email protected]):
dependencies:
cookie-es: 2.0.0
fetchdts: 0.1.7
rou3: 0.7.7
srvx: 0.8.15
optionalDependencies:
crossws: 0.4.1
[email protected]: {}
@@ -18216,7 +18205,7 @@ snapshots:
[email protected]:
dependencies:
detect-libc: 2.1.1
detect-libc: 2.1.2
optionalDependencies:
lightningcss-darwin-arm64: 1.30.1
lightningcss-darwin-x64: 1.30.1
@@ -20339,8 +20328,6 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]:
@@ -20970,7 +20957,7 @@ snapshots:
dependencies:
chokidar: 3.6.0
commander: 9.5.0
get-tsconfig: 4.10.1
get-tsconfig: 4.12.0
globby: 11.1.0
mylas: 2.1.13
normalize-path: 3.0.0