Fix SVG exports in Next.js (#3446)

Next.js bans the use of react-dom/server APIs on the client. React's
docs recommend against using these too:
https://react.dev/reference/react-dom/server/renderToString#removing-rendertostring-from-the-client-code

In this diff, we switch from using `ReactDOMServer.renderToStaticMarkup`
to `ReactDOMClient.createRoot`, fixing SVG exports in next.js apps.
`getSvg` remains deprecated, but we've introduced a new `getSvgElement`
method with a similar API to `getSvgString` - it returns an `{svg,
width, height}` object.

### Change Type

- [x] `sdk` — Changes the tldraw SDK
- [x] `bugfix` — Bug fix
This commit is contained in:
alex 2024-04-11 15:02:05 +01:00 committed by GitHub
parent 84dbf2df20
commit a18525ea78
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 153 additions and 16 deletions

View file

@ -767,6 +767,11 @@ export class Editor extends EventEmitter<TLEventMap> {
getStyleForNextShape<T>(style: StyleProp<T>): T; getStyleForNextShape<T>(style: StyleProp<T>): T;
// @deprecated (undocumented) // @deprecated (undocumented)
getSvg(shapes: TLShape[] | TLShapeId[], opts?: Partial<TLSvgOptions>): Promise<SVGSVGElement | undefined>; getSvg(shapes: TLShape[] | TLShapeId[], opts?: Partial<TLSvgOptions>): Promise<SVGSVGElement | undefined>;
getSvgElement(shapes: TLShape[] | TLShapeId[], opts?: Partial<TLSvgOptions>): Promise<{
svg: SVGSVGElement;
width: number;
height: number;
} | undefined>;
getSvgString(shapes: TLShape[] | TLShapeId[], opts?: Partial<TLSvgOptions>): Promise<{ getSvgString(shapes: TLShape[] | TLShapeId[], opts?: Partial<TLSvgOptions>): Promise<{
svg: string; svg: string;
width: number; width: number;

View file

@ -13888,7 +13888,7 @@
{ {
"kind": "Method", "kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getSvg:member(1)", "canonicalReference": "@tldraw/editor!Editor#getSvg:member(1)",
"docComment": "/**\n * @deprecated\n *\n * Use {@link Editor.getSvgString} instead\n */\n", "docComment": "/**\n * @deprecated\n *\n * Use {@link Editor.getSvgString} or {@link Editor.getSvgElement} instead.\n */\n",
"excerptTokens": [ "excerptTokens": [
{ {
"kind": "Content", "kind": "Content",
@ -13991,6 +13991,112 @@
"isAbstract": false, "isAbstract": false,
"name": "getSvg" "name": "getSvg"
}, },
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getSvgElement:member(1)",
"docComment": "/**\n * Get an exported SVG element of the given shapes.\n *\n * @param ids - The shapes (or shape ids) to export.\n *\n * @param opts - Options for the export.\n *\n * @returns The SVG element.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "getSvgElement(shapes: "
},
{
"kind": "Reference",
"text": "TLShape",
"canonicalReference": "@tldraw/tlschema!TLShape:type"
},
{
"kind": "Content",
"text": "[] | "
},
{
"kind": "Reference",
"text": "TLShapeId",
"canonicalReference": "@tldraw/tlschema!TLShapeId:type"
},
{
"kind": "Content",
"text": "[]"
},
{
"kind": "Content",
"text": ", opts?: "
},
{
"kind": "Reference",
"text": "Partial",
"canonicalReference": "!Partial:type"
},
{
"kind": "Content",
"text": "<"
},
{
"kind": "Reference",
"text": "TLSvgOptions",
"canonicalReference": "@tldraw/editor!TLSvgOptions:type"
},
{
"kind": "Content",
"text": ">"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Reference",
"text": "Promise",
"canonicalReference": "!Promise:interface"
},
{
"kind": "Content",
"text": "<{\n svg: "
},
{
"kind": "Reference",
"text": "SVGSVGElement",
"canonicalReference": "!SVGSVGElement:interface"
},
{
"kind": "Content",
"text": ";\n width: number;\n height: number;\n } | undefined>"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 11,
"endIndex": 15
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [
{
"parameterName": "shapes",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 5
},
"isOptional": false
},
{
"parameterName": "opts",
"parameterTypeTokenRange": {
"startIndex": 6,
"endIndex": 10
},
"isOptional": true
}
],
"isOptional": false,
"isAbstract": false,
"name": "getSvgElement"
},
{ {
"kind": "Method", "kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getSvgString:member(1)", "canonicalReference": "@tldraw/editor!Editor#getSvgString:member(1)",

View file

@ -61,7 +61,6 @@ import {
import { EventEmitter } from 'eventemitter3' import { EventEmitter } from 'eventemitter3'
import { flushSync } from 'react-dom' import { flushSync } from 'react-dom'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { renderToStaticMarkup } from 'react-dom/server'
import { TLUser, createTLUser } from '../config/createTLUser' import { TLUser, createTLUser } from '../config/createTLUser'
import { checkShapesAndAddCore } from '../config/defaultShapes' import { checkShapesAndAddCore } from '../config/defaultShapes'
import { import {
@ -8070,6 +8069,33 @@ export class Editor extends EventEmitter<TLEventMap> {
return this return this
} }
/**
* Get an exported SVG element of the given shapes.
*
* @param ids - The shapes (or shape ids) to export.
* @param opts - Options for the export.
*
* @returns The SVG element.
*
* @public
*/
async getSvgElement(shapes: TLShapeId[] | TLShape[], opts = {} as Partial<TLSvgOptions>) {
const result = await getSvgJsx(this, shapes, opts)
if (!result) return undefined
const fragment = document.createDocumentFragment()
const root = createRoot(fragment)
flushSync(() => {
root.render(result.jsx)
})
const svg = fragment.firstElementChild
assert(svg instanceof SVGSVGElement, 'Expected an SVG element')
root.unmount()
return { svg, width: result.width, height: result.height }
}
/** /**
* Get an exported SVG string of the given shapes. * Get an exported SVG string of the given shapes.
* *
@ -8081,21 +8107,22 @@ export class Editor extends EventEmitter<TLEventMap> {
* @public * @public
*/ */
async getSvgString(shapes: TLShapeId[] | TLShape[], opts = {} as Partial<TLSvgOptions>) { async getSvgString(shapes: TLShapeId[] | TLShape[], opts = {} as Partial<TLSvgOptions>) {
const svg = await getSvgJsx(this, shapes, opts) const result = await this.getSvgElement(shapes, opts)
if (!svg) return undefined if (!result) return undefined
return { svg: renderToStaticMarkup(svg.jsx), width: svg.width, height: svg.height }
const serializer = new XMLSerializer()
return {
svg: serializer.serializeToString(result.svg),
width: result.width,
height: result.height,
}
} }
/** @deprecated Use {@link Editor.getSvgString} instead */ /** @deprecated Use {@link Editor.getSvgString} or {@link Editor.getSvgElement} instead. */
async getSvg(shapes: TLShapeId[] | TLShape[], opts = {} as Partial<TLSvgOptions>) { async getSvg(shapes: TLShapeId[] | TLShape[], opts = {} as Partial<TLSvgOptions>) {
const svg = await getSvgJsx(this, shapes, opts) const result = await this.getSvgElement(shapes, opts)
if (!svg) return undefined if (!result) return undefined
const fragment = new DocumentFragment() return result.svg
const root = createRoot(fragment)
flushSync(() => root.render(svg.jsx))
const rendered = fragment.firstElementChild
root.unmount()
return rendered as SVGSVGElement
} }
/* --------------------- Events --------------------- */ /* --------------------- Events --------------------- */

View file

@ -184,7 +184,6 @@ export async function getSvgJsx(
const svg = ( const svg = (
<SvgExportContextProvider editor={editor} context={exportContext}> <SvgExportContextProvider editor={editor} context={exportContext}>
<svg <svg
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio={preserveAspectRatio ? preserveAspectRatio : undefined} preserveAspectRatio={preserveAspectRatio ? preserveAspectRatio : undefined}
direction="ltr" direction="ltr"
width={w} width={w}

View file

@ -7,7 +7,7 @@ exports[`Matches a snapshot: Basic SVG 1`] = `
height="564" height="564"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
style="background-color:transparent" style="background-color: transparent;"
viewBox="-32 -32 564 564" viewBox="-32 -32 564 564"
width="564" width="564"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"