Skip to content

Commit 9e28418

Browse files
committed
feat(md): support load remote react component
Signed-off-by: Innei <[email protected]>
1 parent cfb8c6a commit 9e28418

File tree

12 files changed

+246
-24
lines changed

12 files changed

+246
-24
lines changed

src/app/(app)/(home)/page.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ const Welcome = () => {
222222

223223
const PostScreen = () => {
224224
const { posts } = useHomeQueryData()
225+
225226
return (
226227
<Screen className="h-fit min-h-[120vh]">
227228
<TwoColumnLayout leftContainerClassName="h-[30rem] lg:h-1/2">

src/app/(app)/layout.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { PropsWithChildren } from 'react'
77
import { ClerkProvider } from '@clerk/nextjs'
88

99
import PKG from '~/../package.json'
10+
import { Global } from '~/components/common/Global'
1011
import { HydrationEndDetector } from '~/components/common/HydrationEndDetector'
1112
import { ScrollTop } from '~/components/common/ScrollTop'
1213
import { Root } from '~/components/layout/root/Root'
@@ -145,6 +146,7 @@ export default async function RootLayout(props: PropsWithChildren) {
145146
suppressHydrationWarning
146147
>
147148
<head>
149+
<Global />
148150
<SayHi />
149151
<HydrationEndDetector />
150152
<AccentColorStyleInjector color={themeConfig.config.color} />

src/components/common/Global.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
'use client'
2+
3+
import React, { useLayoutEffect } from 'react'
4+
import ReactDOM from 'react-dom'
5+
6+
import * as Button from '~/components/ui/button'
7+
8+
export const Global = () => {
9+
useLayoutEffect(() => {
10+
Object.assign(window, {
11+
React,
12+
ReactDOM,
13+
ShiroComponents: { ...Button },
14+
})
15+
}, [])
16+
return null
17+
}

src/components/modules/comment/CommentMarkdown.tsx

+14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { MarkdownToJSX } from '~/components/ui/markdown'
22
import type { FC } from 'react'
33

4+
import { HighLighter } from '~/components/ui/code-highlighter'
45
import { Markdown } from '~/components/ui/markdown'
56

67
const disabledTypes = [
@@ -23,6 +24,19 @@ export const CommentMarkdown: FC<{
2324
disableParsingRawHTML
2425
forceBlock
2526
value={children}
27+
extendsRules={{
28+
codeBlock: {
29+
react(node, output, state) {
30+
return (
31+
<HighLighter
32+
key={state?.key}
33+
content={node.content}
34+
lang={node.lang}
35+
/>
36+
)
37+
},
38+
},
39+
}}
2640
/>
2741
)
2842
}

src/components/modules/shared/BlockLoading.tsx

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
import type { FC, PropsWithChildren } from 'react'
1+
import { clsxm } from '~/lib/helper'
22

3-
export const BlockLoading: FC<PropsWithChildren> = (props) => {
3+
export const BlockLoading: Component = (props) => {
44
return (
5-
<div className="flex h-[500px] items-center justify-center rounded-lg bg-slate-100 text-sm dark:bg-neutral-800">
5+
<div
6+
className={clsxm(
7+
'flex h-[500px] items-center justify-center rounded-lg bg-slate-100 text-sm dark:bg-neutral-800',
8+
props.className,
9+
)}
10+
>
611
{props.children}
712
</div>
813
)

src/components/modules/shared/CodeBlock.tsx

+23-15
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ReactComponentRender } from '~/components/ui/react-component-render'
12
import { lazy, Suspense, useMemo, useState } from 'react'
23
import { useIsomorphicLayoutEffect } from 'foxact/use-isomorphic-layout-effect'
34
import dynamic from 'next/dynamic'
@@ -26,25 +27,32 @@ const ExcalidrawLazy = ({ data }: any) => {
2627
</Suspense>
2728
)
2829
}
29-
export const CodeBlock = (props: {
30+
export const CodeBlockRender = (props: {
3031
lang: string | undefined
3132
content: string
3233
}) => {
3334
const Content = useMemo(() => {
34-
if (props.lang === 'mermaid') {
35-
const Mermaid = dynamic(() =>
36-
import('./Mermaid').then((mod) => mod.Mermaid),
37-
)
38-
return <Mermaid {...props} />
39-
} else if (props.lang === 'excalidraw') {
40-
return <ExcalidrawLazy data={props.content} />
41-
} else {
42-
const HighLighter = dynamic(() =>
43-
import('~/components/ui/code-highlighter/CodeHighlighter').then(
44-
(mod) => mod.HighLighter,
45-
),
46-
)
47-
return <HighLighter {...props} />
35+
switch (props.lang) {
36+
case 'mermaid': {
37+
const Mermaid = dynamic(() =>
38+
import('./Mermaid').then((mod) => mod.Mermaid),
39+
)
40+
return <Mermaid {...props} />
41+
}
42+
case 'excalidraw': {
43+
return <ExcalidrawLazy data={props.content} />
44+
}
45+
case 'component': {
46+
return <ReactComponentRender dls={props.content} />
47+
}
48+
default: {
49+
const HighLighter = dynamic(() =>
50+
import('~/components/ui/code-highlighter/CodeHighlighter').then(
51+
(mod) => mod.HighLighter,
52+
),
53+
)
54+
return <HighLighter {...props} />
55+
}
4856
}
4957
}, [props])
5058

src/components/ui/markdown/Markdown.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ import { getFootNoteDomId, getFootNoteRefDomId } from './utils/get-id'
4242
import { redHighlight } from './utils/redHighlight'
4343

4444
const CodeBlock = dynamic(() =>
45-
import('~/components/modules/shared/CodeBlock').then((mod) => mod.CodeBlock),
45+
import('~/components/modules/shared/CodeBlock').then(
46+
(mod) => mod.CodeBlockRender,
47+
),
4648
)
4749

4850
export interface MdProps {

src/components/ui/markdown/customize.md

+28-1
Original file line numberDiff line numberDiff line change
@@ -382,4 +382,31 @@ P\left(U,T\right)=100\left.\left(0.6\min\left(1,\frac{U-0.70}{0.90-0.70}\right)+
382382
383383
$$
384384

385-
-
385+
386+
## Excalidraw
387+
388+
```excalidraw
389+
{"type":"excalidraw/clipboard","elements":[{"type":"rectangle","version":14,"versionNonce":1361369853,"isDeleted":false,"id":"_PSpf6pLwkWIJubC_tf9D","fillStyle":"solid","strokeWidth":2,"strokeStyle":"solid","roughness":1,"opacity":100,"angle":0,"x":545.0390625,"y":387.296875,"strokeColor":"#1e1e1e","backgroundColor":"transparent","width":177.53515625,"height":138.328125,"seed":1495751197,"groupIds":[],"frameId":null,"roundness":{"type":3},"boundElements":[],"updated":1706954302946,"link":null,"locked":false}],"files":{}}
390+
```
391+
392+
````markdown
393+
```excalidraw
394+
{"type":"excalidraw/clipboard","elements":[{"type":"rectangle","version":14,"versionNonce":1361369853,"isDeleted":false,"id":"_PSpf6pLwkWIJubC_tf9D","fillStyle":"solid","strokeWidth":2,"strokeStyle":"solid","roughness":1,"opacity":100,"angle":0,"x":545.0390625,"y":387.296875,"strokeColor":"#1e1e1e","backgroundColor":"transparent","width":177.53515625,"height":138.328125,"seed":1495751197,"groupIds":[],"frameId":null,"roundness":{"type":3},"boundElements":[],"updated":1706954302946,"link":null,"locked":false}],"files":{}}
395+
```
396+
````
397+
398+
## React Remote Component Render
399+
400+
```component
401+
import=http://127.0.0.1:2333/snippets/js/components
402+
name=MyComponents.Card
403+
```
404+
405+
````markdown
406+
```component
407+
import=http://127.0.0.1:2333/snippets/js/components
408+
name=MyComponents.Card
409+
```
410+
````
411+
412+

src/components/ui/markdown/index.demo.tsx

+63-4
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,90 @@
11
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
2+
import { ReactComponentRender } from '~/components/ui/react-component-render/ComponentRender'
3+
import { lazy, Suspense, useMemo, useState } from 'react'
24
import { ToastContainer } from 'react-toastify'
5+
import { useIsomorphicLayoutEffect } from 'foxact/use-isomorphic-layout-effect'
36
import { ThemeProvider } from 'next-themes'
7+
import type { ReactNode } from 'react'
48
import type { DocumentComponent } from 'storybook/typings'
59

10+
import { BlockLoading } from '~/components/modules/shared/BlockLoading'
11+
import { Mermaid } from '~/components/modules/shared/Mermaid'
12+
import { ExcalidrawLoading } from '~/components/ui/excalidraw/ExcalidrawLoading'
13+
14+
import { HighLighter } from '../code-highlighter'
615
// @ts-expect-error
716
import customize from './customize.md?raw'
817
import { Markdown } from './Markdown'
918

1019
const queryClient = new QueryClient()
20+
21+
const ExcalidrawLazy = ({ data }: any) => {
22+
const [Excalidraw, setComponent] = useState(null as ReactNode)
23+
24+
useIsomorphicLayoutEffect(() => {
25+
const Component = lazy(() =>
26+
import('~/components/ui/excalidraw').then((mod) => ({
27+
default: mod.Excalidraw,
28+
})),
29+
)
30+
31+
setComponent(<Component key={data} data={data} />)
32+
}, [data])
33+
34+
return (
35+
<Suspense fallback={<ExcalidrawLoading />}>
36+
{Excalidraw ?? <ExcalidrawLoading />}
37+
</Suspense>
38+
)
39+
}
40+
const CodeBlockRender = (props: {
41+
lang: string | undefined
42+
content: string
43+
}) => {
44+
const Content = useMemo(() => {
45+
switch (props.lang) {
46+
case 'mermaid': {
47+
return <Mermaid {...props} />
48+
}
49+
case 'excalidraw': {
50+
return <ExcalidrawLazy data={props.content} />
51+
}
52+
case 'component': {
53+
return <ReactComponentRender dls={props.content} />
54+
}
55+
default: {
56+
return <HighLighter {...props} />
57+
}
58+
}
59+
}, [props])
60+
61+
return (
62+
<Suspense fallback={<BlockLoading>CodeBlock Loading...</BlockLoading>}>
63+
{Content}
64+
</Suspense>
65+
)
66+
}
67+
1168
export const MarkdownCustomize: DocumentComponent = () => {
1269
return (
1370
<QueryClientProvider client={queryClient}>
1471
<ThemeProvider>
1572
<main className="relative m-auto mt-6 max-w-[800px]">
1673
<Markdown
74+
value={customize}
1775
extendsRules={{
1876
codeBlock: {
1977
react(node, output, state) {
2078
return (
21-
<pre>
22-
<code>{node.content}</code>
23-
</pre>
79+
<CodeBlockRender
80+
key={state?.key}
81+
content={node.content}
82+
lang={node.lang}
83+
/>
2484
)
2585
},
2686
},
2787
}}
28-
value={customize}
2988
className="prose"
3089
as="article"
3190
/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { Suspense, useState } from 'react'
2+
import { ErrorBoundary } from 'react-error-boundary'
3+
import { useIsomorphicLayoutEffect } from 'framer-motion'
4+
import type { FC } from 'react'
5+
6+
import { BlockLoading } from '~/components/modules/shared/BlockLoading'
7+
import { loadScript } from '~/lib/load-script'
8+
import { get } from '~/lib/lodash'
9+
10+
export interface ReactComponentRenderProps {
11+
dls: string
12+
}
13+
/**
14+
* define the render dls of the component
15+
* ```component
16+
* import=http://127.0.0.1:2333/snippets/js/components.js
17+
* name=Components.Card
18+
* ```
19+
*
20+
* name will be used to find the component in the import, if in the nested object, use dot to separate
21+
*
22+
*/
23+
export const ReactComponentRender: FC<ReactComponentRenderProps> = (props) => {
24+
const { dls } = props
25+
const [Component, setComponent] = useState({
26+
component: ComponentBlockLoading,
27+
})
28+
29+
useIsomorphicLayoutEffect(() => {
30+
const props = parseDlsContent(dls)
31+
32+
loadScript(
33+
'https://unpkg.com/styled-components/dist/styled-components.min.js',
34+
)
35+
.then(() => loadScript(props.import))
36+
.then(() => {
37+
const Component = get(window, props.name)
38+
console.log('Component', Component)
39+
setComponent({ component: Component })
40+
})
41+
}, [dls])
42+
43+
return (
44+
<ErrorBoundary FallbackComponent={ComponentBlockError}>
45+
<Suspense fallback={<ComponentBlockLoading />}>
46+
<Component.component />
47+
</Suspense>
48+
</ErrorBoundary>
49+
)
50+
}
51+
52+
const ComponentBlockError = () => {
53+
return (
54+
<BlockLoading className="bg-red-300 dark:bg-red-700">
55+
Component Error
56+
</BlockLoading>
57+
)
58+
}
59+
const ComponentBlockLoading = () => {
60+
return <BlockLoading>Component Loading...</BlockLoading>
61+
}
62+
63+
function parseDlsContent(dls: string) {
64+
const parsedProps = {} as {
65+
name: string
66+
import: string
67+
}
68+
dls.split('\n').forEach((line) => {
69+
const [key, value] = line.split('=')
70+
;(parsedProps as any)[key] = value
71+
})
72+
73+
return parsedProps
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './ComponentRender'

src/lib/lodash.ts

+12
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,15 @@ export function uniqBy<T, K>(array: T[], iteratee: (item: T) => K): T[] {
156156
return false
157157
})
158158
}
159+
160+
export function get(target: object, path: string) {
161+
const keys = path.split('.')
162+
let result = target as any
163+
for (const key of keys) {
164+
result = result[key]
165+
if (result === undefined) {
166+
return result
167+
}
168+
}
169+
return result
170+
}

0 commit comments

Comments
 (0)