Skip to content

Commit 94c4484

Browse files
committed
fix #600
1 parent 9bc47fd commit 94c4484

File tree

4 files changed

+57
-2
lines changed

4 files changed

+57
-2
lines changed

src/layout.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
buildXMLString,
1313
normalizeChildren,
1414
hasDangerouslySetInnerHTMLProp,
15+
isReactComponent,
16+
isForwardRefComponent,
1517
} from './utils.js'
1618
import { SVGNodeToImage } from './handler/preprocess.js'
1719
import computeStyle from './handler/compute.js'
@@ -79,7 +81,7 @@ export default async function* layout(
7981
}
8082

8183
// Not a regular element.
82-
if (!isReactElement(element) || typeof element.type === 'function') {
84+
if (!isReactElement(element) || isReactComponent(element.type)) {
8385
let iter: ReturnType<typeof layout>
8486

8587
if (!isReactElement(element)) {
@@ -90,11 +92,22 @@ export default async function* layout(
9092
if (isClass(element.type as Function)) {
9193
throw new Error('Class component is not supported.')
9294
}
95+
96+
let render: Function
97+
98+
// This is a hack to support React.forwardRef wrapped components.
99+
// https://github.com/vercel/satori/issues/600
100+
if (isForwardRefComponent(element.type)) {
101+
render = (element.type as any).render
102+
} else {
103+
render = element.type as Function
104+
}
105+
93106
// If it's a custom component, Satori strictly requires it to be pure,
94107
// stateless, and not relying on any React APIs such as hooks or suspense.
95108
// So we can safely evaluate it to render. Otherwise, an error will be
96109
// thrown by React.
97-
iter = layout((element.type as Function)(element.props), context)
110+
iter = layout(render(element.props), context)
98111
yield (await iter.next()).value as { word: string; locale?: string }[]
99112
}
100113

src/utils.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ export function isClass(f: Function) {
2020
return /^class\s/.test(f.toString())
2121
}
2222

23+
export function isForwardRefComponent(type: any) {
24+
return type && type.$$typeof === Symbol.for('react.forward_ref')
25+
}
26+
27+
export function isReactComponent(type: any) {
28+
return typeof type === 'function' || isForwardRefComponent(type)
29+
}
30+
2331
export function hasDangerouslySetInnerHTMLProp(props: any) {
2432
return 'dangerouslySetInnerHTML' in props
2533
}

test/react.test.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { forwardRef } from 'react'
2+
import { it, describe, expect } from 'vitest'
3+
4+
import { initFonts, toImage } from './utils.js'
5+
import satori from '../src/index.js'
6+
7+
describe('React APIs', () => {
8+
let fonts: any
9+
initFonts((f) => (fonts = f))
10+
11+
it('should support `forwardRef` wrapped components', async () => {
12+
const Foo = forwardRef(function _() {
13+
return <div>hello</div>
14+
})
15+
16+
const svg = await satori(
17+
<div
18+
style={{
19+
display: 'flex',
20+
color: 'red',
21+
fontSize: 14,
22+
}}
23+
>
24+
<Foo />
25+
</div>,
26+
{
27+
width: 100,
28+
height: 100,
29+
fonts,
30+
}
31+
)
32+
expect(toImage(svg, 100)).toMatchImageSnapshot()
33+
})
34+
})

0 commit comments

Comments
 (0)