From 94c448451a827aea06fdea5c31d87c31b44a2331 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Mon, 11 Mar 2024 19:45:16 +0100 Subject: [PATCH] fix #600 --- src/layout.ts | 17 +++++++-- src/utils.ts | 8 +++++ ...-forward-ref-wrapped-components-1-snap.png | Bin 0 -> 800 bytes test/react.test.tsx | 34 ++++++++++++++++++ 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 test/__image_snapshots__/react-test-tsx-test-react-test-tsx-react-ap-is-should-support-forward-ref-wrapped-components-1-snap.png create mode 100644 test/react.test.tsx diff --git a/src/layout.ts b/src/layout.ts index 70734036..9fbbacd0 100644 --- a/src/layout.ts +++ b/src/layout.ts @@ -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' @@ -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 if (!isReactElement(element)) { @@ -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 }[] } diff --git a/src/utils.ts b/src/utils.ts index c672766f..21d3dd11 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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 } diff --git a/test/__image_snapshots__/react-test-tsx-test-react-test-tsx-react-ap-is-should-support-forward-ref-wrapped-components-1-snap.png b/test/__image_snapshots__/react-test-tsx-test-react-test-tsx-react-ap-is-should-support-forward-ref-wrapped-components-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..0a27062de8470fc0aa086d6c4eaedeac7d951c44 GIT binary patch literal 800 zcmeAS@N?(olHy`uVBq!ia0vp^DImFa^lgRtQV5>bQSy9h*Vrgyp{r_8Wiy^Z(47Lze&D z8~hG-aTqE)oylgS1R@ifadRZUN7v zrT$NBBPVFR+&7u;%EKbZpFUNd)}Ics-Vpz_DBbDCd5y!>-k#N-(Ry+#9x|O- z&fB+CK4s-Z-IXzVJ7-+9pSV6{ap0+0cS?AEo+wuO(z>t1x+?7VqWhZmo)>?V%6YzB zB5FFP@#dC;8~yfgx*=j%@y+(H!5-1SF~V~~C-yy$`|3Jb;FtBU{3l(18&?&lKU6)l zl((xi@Wix#vNtE%pE;_&?D5XWKm6wYS~Pir63Yes74;A2l>UkXsq^h9l@gk*l(OOd zOLM9AT}tO~7Vm$)uj>NP-IFIg{MrBI`W3cx<5T)Yw?FnBvNxD+rg1IPBd2S^!F|i) zFRnjn`kOh_J=1T$_=Rx6Z$NjazhqY|_VjwymKlPI eX~UekpYhznO{r!*Y?Z*2&*16m=d#Wzp$PzA_fxO{ literal 0 HcmV?d00001 diff --git a/test/react.test.tsx b/test/react.test.tsx new file mode 100644 index 00000000..94360a70 --- /dev/null +++ b/test/react.test.tsx @@ -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
hello
+ }) + + const svg = await satori( +
+ +
, + { + width: 100, + height: 100, + fonts, + } + ) + expect(toImage(svg, 100)).toMatchImageSnapshot() + }) +})