Skip to content

Commit

Permalink
fix #600
Browse files Browse the repository at this point in the history
  • Loading branch information
shuding committed Mar 11, 2024
1 parent 9bc47fd commit 94c4484
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 2 deletions.
17 changes: 15 additions & 2 deletions src/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
buildXMLString,
normalizeChildren,
hasDangerouslySetInnerHTMLProp,
isReactComponent,
isForwardRefComponent,
} from './utils.js'
import { SVGNodeToImage } from './handler/preprocess.js'
import computeStyle from './handler/compute.js'
Expand Down Expand Up @@ -79,7 +81,7 @@ export default async function* layout(
}

// Not a regular element.
if (!isReactElement(element) || typeof element.type === 'function') {
if (!isReactElement(element) || isReactComponent(element.type)) {
let iter: ReturnType<typeof layout>

if (!isReactElement(element)) {
Expand All @@ -90,11 +92,22 @@ export default async function* layout(
if (isClass(element.type as Function)) {
throw new Error('Class component is not supported.')
}

let render: Function

// This is a hack to support React.forwardRef wrapped components.
// https://github.com/vercel/satori/issues/600
if (isForwardRefComponent(element.type)) {
render = (element.type as any).render
} else {
render = element.type as Function
}

// If it's a custom component, Satori strictly requires it to be pure,
// stateless, and not relying on any React APIs such as hooks or suspense.
// So we can safely evaluate it to render. Otherwise, an error will be
// thrown by React.
iter = layout((element.type as Function)(element.props), context)
iter = layout(render(element.props), context)
yield (await iter.next()).value as { word: string; locale?: string }[]
}

Expand Down
8 changes: 8 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ export function isClass(f: Function) {
return /^class\s/.test(f.toString())
}

export function isForwardRefComponent(type: any) {
return type && type.$$typeof === Symbol.for('react.forward_ref')
}

export function isReactComponent(type: any) {
return typeof type === 'function' || isForwardRefComponent(type)
}

export function hasDangerouslySetInnerHTMLProp(props: any) {
return 'dangerouslySetInnerHTML' in props
}
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions test/react.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { forwardRef } from 'react'
import { it, describe, expect } from 'vitest'

import { initFonts, toImage } from './utils.js'
import satori from '../src/index.js'

describe('React APIs', () => {
let fonts: any
initFonts((f) => (fonts = f))

it('should support `forwardRef` wrapped components', async () => {
const Foo = forwardRef(function _() {
return <div>hello</div>
})

const svg = await satori(
<div
style={{
display: 'flex',
color: 'red',
fontSize: 14,
}}
>
<Foo />
</div>,
{
width: 100,
height: 100,
fonts,
}
)
expect(toImage(svg, 100)).toMatchImageSnapshot()
})
})

0 comments on commit 94c4484

Please sign in to comment.