11'use client' ;
22
3+ import collections from 'collections/browser' ;
34import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock' ;
45import * as lucide from 'lucide-react' ;
56import Markdown , { RuleType } from 'markdown-to-jsx' ;
6- import { useEffect , useRef } from 'react' ;
7+ import {
8+ Component ,
9+ type ComponentProps ,
10+ type ComponentType ,
11+ type PropsWithChildren ,
12+ Suspense ,
13+ useEffect ,
14+ useRef ,
15+ } from 'react' ;
716
8- import { useMDXComponents } from '@/components/mdx' ;
17+ import { getMDXComponents , useMDXComponents } from '@/components/mdx' ;
918import { gitConfig } from '@/lib/shared' ;
1019
20+ const officialPluginDocs = collections . docs . createClientLoader < { slug : string } > ( {
21+ component ( doc , { slug } ) {
22+ const MDX = doc . default ;
23+ const components = getMDXComponents ( ) ;
24+ const Link = components . a as ComponentType < ComponentProps < 'a' > > ;
25+
26+ return (
27+ < MDX
28+ components = { getMDXComponents ( {
29+ a : ( { href, ...props } ) => {
30+ if ( ! href || href . startsWith ( '#' ) || href . startsWith ( '/' ) ) {
31+ return < Link href = { href } { ...props } /> ;
32+ }
33+
34+ const base = new URL ( `https://fraq.dev/docs/plugins/${ slug } .mdx` ) ;
35+ const resolved = new URL ( href , base ) ;
36+ if ( resolved . origin !== base . origin ) {
37+ return < Link href = { href } { ...props } /> ;
38+ }
39+
40+ resolved . pathname = resolved . pathname . replace ( / \. m d x ? $ / , '' ) ;
41+ return < Link href = { `${ resolved . pathname } ${ resolved . search } ${ resolved . hash } ` } { ...props } /> ;
42+ } ,
43+ } ) }
44+ />
45+ ) ;
46+ } ,
47+ } ) ;
48+
49+ function OfficialPluginDoc ( { path, slug } : { path : string ; slug : string } ) {
50+ const MDX = officialPluginDocs . getComponent ( path ) ;
51+ return < MDX slug = { slug } /> ;
52+ }
53+
54+ class OfficialPluginDocErrorBoundary extends Component < PropsWithChildren , { failed : boolean } > {
55+ state = { failed : false } ;
56+
57+ static getDerivedStateFromError ( ) {
58+ return { failed : true } ;
59+ }
60+
61+ render ( ) {
62+ if ( this . state . failed ) {
63+ return < p className = "text-sm text-fd-muted-foreground" > 官方文档加载失败,请前往文档页面查看。</ p > ;
64+ }
65+ return this . props . children ;
66+ }
67+ }
68+
1169export interface PluginEntry {
1270 id : string ;
1371 name : string ;
@@ -32,13 +90,6 @@ export function officialPluginSlug(plugin: PluginEntry): string {
3290 return plugin . id . slice ( 'fraqjs/' . length ) ;
3391}
3492
35- function pluginHref ( plugin : PluginEntry ) : string {
36- if ( isOfficial ( plugin ) ) {
37- return `/docs/plugins/${ officialPluginSlug ( plugin ) } ` ;
38- }
39- return plugin . repository ;
40- }
41-
4293function repositorySlug ( plugin : PluginEntry ) : string | null {
4394 if ( plugin . repository . startsWith ( 'https://github.com/' ) ) {
4495 const parts = plugin . repository . replace ( 'https://github.com/' , '' ) . split ( '/' ) ;
@@ -96,8 +147,8 @@ export function ReadmeDrawer({
96147 const githubUrl = official
97148 ? `https://github.com/${ gitConfig . user } /${ gitConfig . repo } /tree/${ gitConfig . branch } /plugins/${ slug } `
98149 : plugin . repository ;
99- const docsUrl = official ? pluginHref ( plugin ) : null ;
100150 const npmUrl = `https://www.npmjs.com/package/${ npmId } ` ;
151+ const officialDocPath = slug ? `plugins/${ slug } .mdx` : null ;
101152
102153 return (
103154 < >
@@ -142,17 +193,6 @@ export function ReadmeDrawer({
142193
143194 { /* Action buttons */ }
144195 < div className = "flex shrink-0 items-center gap-2 border-b border-fd-border px-5 py-3" >
145- { docsUrl && (
146- < a
147- href = { docsUrl }
148- target = "_blank"
149- rel = "noopener noreferrer"
150- className = "inline-flex items-center gap-1.5 rounded-md border border-fd-border bg-fd-muted/60 px-3 py-1.5 text-xs text-fd-foreground transition-colors hover:bg-fd-accent"
151- >
152- < lucide . BookOpenIcon className = "size-3.5" />
153- 文档
154- </ a >
155- ) }
156196 < a
157197 href = { npmUrl }
158198 target = "_blank"
@@ -181,46 +221,59 @@ export function ReadmeDrawer({
181221
182222 { /* Documentation content */ }
183223 < div ref = { scrollRef } className = "min-h-0 flex-1 overflow-y-auto px-5 py-5" >
184- { readme . loading && (
185- < div className = "flex items-center gap-2 text-sm text-fd-muted-foreground" >
186- < lucide . LoaderCircleIcon className = "size-4 animate-spin" />
187- 正在加载 README…
188- </ div >
189- ) }
190- { readme . error && (
191- < p className = "text-sm text-fd-muted-foreground" > README 加载失败,请前往官方文档、npm 或 GitHub 查看。</ p >
192- ) }
193- { readme . text && official ? (
194- < div className = "flex flex-col gap-4" >
195- < p className = "text-sm text-fd-muted-foreground" >
196- 以下是官方文档的原始 Markdown 内容,请点击“文档”按钮查看完整内容。
197- </ p >
198- { /* divider */ }
199- < div className = "h-px bg-fd-border" />
200- < pre className = "whitespace-pre-wrap break-words font-mono text-xs leading-relaxed text-fd-foreground" >
201- { readme . text }
202- </ pre >
203- </ div >
204- ) : readme . text ? (
205- < Markdown
206- options = { {
207- overrides : { ...mdxComponents , img : ( ) => null } ,
208- renderRule ( next , node , _renderChildren , state ) {
209- if ( node . type === RuleType . codeBlock ) {
210- return (
211- < div key = { state . key } className = "[&_code]:text-[0.7109375rem]" >
212- < DynamicCodeBlock lang = { node . lang || 'text' } code = { node . text } />
213- </ div >
214- ) ;
224+ { official && slug && officialDocPath ? (
225+ Object . hasOwn ( collections . docs . raw , officialDocPath ) ? (
226+ < OfficialPluginDocErrorBoundary key = { officialDocPath } >
227+ < Suspense
228+ fallback = {
229+ < div className = "flex items-center gap-2 text-sm text-fd-muted-foreground" >
230+ < lucide . LoaderCircleIcon className = "size-4 animate-spin" />
231+ 正在加载官方文档…
232+ </ div >
215233 }
216- return next ( ) ;
217- } ,
218- } }
219- className = "prose prose-sm dark:prose-invert max-w-none text-fd-foreground [&_a]:text-fd-primary"
220- >
221- { readme . text }
222- </ Markdown >
223- ) : null }
234+ >
235+ < div className = "prose prose-sm dark:prose-invert max-w-none text-fd-foreground [&_a]:text-fd-primary" >
236+ < Markdown options = { { overrides : mdxComponents } } > { `# ${ plugin . name } ` } </ Markdown >
237+ < OfficialPluginDoc path = { officialDocPath } slug = { slug } />
238+ </ div >
239+ </ Suspense >
240+ </ OfficialPluginDocErrorBoundary >
241+ ) : (
242+ < p className = "text-sm text-fd-muted-foreground" > 该官方插件暂时没有可用的文档。</ p >
243+ )
244+ ) : (
245+ < >
246+ { readme . loading && (
247+ < div className = "flex items-center gap-2 text-sm text-fd-muted-foreground" >
248+ < lucide . LoaderCircleIcon className = "size-4 animate-spin" />
249+ 正在加载 README…
250+ </ div >
251+ ) }
252+ { readme . error && (
253+ < p className = "text-sm text-fd-muted-foreground" > README 加载失败,请前往 npm 或 GitHub 查看。</ p >
254+ ) }
255+ { readme . text && (
256+ < Markdown
257+ options = { {
258+ overrides : { ...mdxComponents , img : ( ) => null } ,
259+ renderRule ( next , node , _renderChildren , state ) {
260+ if ( node . type === RuleType . codeBlock ) {
261+ return (
262+ < div key = { state . key } className = "[&_code]:text-[0.7109375rem]" >
263+ < DynamicCodeBlock lang = { node . lang || 'text' } code = { node . text } />
264+ </ div >
265+ ) ;
266+ }
267+ return next ( ) ;
268+ } ,
269+ } }
270+ className = "prose prose-sm dark:prose-invert max-w-none text-fd-foreground [&_a]:text-fd-primary"
271+ >
272+ { readme . text }
273+ </ Markdown >
274+ ) }
275+ </ >
276+ ) }
224277 </ div >
225278 </ div >
226279 </ >
0 commit comments