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
pull/3451/head
alex 2024-04-11 15:02:05 +01:00 zatwierdzone przez GitHub
rodzic 84dbf2df20
commit a18525ea78
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
5 zmienionych plików z 153 dodań i 16 usunięć

Wyświetl plik

@ -767,6 +767,11 @@ export class Editor extends EventEmitter<TLEventMap> {
getStyleForNextShape<T>(style: StyleProp<T>): T;
// @deprecated (undocumented)
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<{
svg: string;
width: number;

Wyświetl plik

@ -13888,7 +13888,7 @@
{
"kind": "Method",
"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": [
{
"kind": "Content",
@ -13991,6 +13991,112 @@
"isAbstract": false,
"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",
"canonicalReference": "@tldraw/editor!Editor#getSvgString:member(1)",

Wyświetl plik

@ -61,7 +61,6 @@ import {
import { EventEmitter } from 'eventemitter3'
import { flushSync } from 'react-dom'
import { createRoot } from 'react-dom/client'
import { renderToStaticMarkup } from 'react-dom/server'
import { TLUser, createTLUser } from '../config/createTLUser'
import { checkShapesAndAddCore } from '../config/defaultShapes'
import {
@ -8070,6 +8069,33 @@ export class Editor extends EventEmitter<TLEventMap> {
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.
*
@ -8081,21 +8107,22 @@ export class Editor extends EventEmitter<TLEventMap> {
* @public
*/
async getSvgString(shapes: TLShapeId[] | TLShape[], opts = {} as Partial<TLSvgOptions>) {
const svg = await getSvgJsx(this, shapes, opts)
if (!svg) return undefined
return { svg: renderToStaticMarkup(svg.jsx), width: svg.width, height: svg.height }
const result = await this.getSvgElement(shapes, opts)
if (!result) return undefined
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>) {
const svg = await getSvgJsx(this, shapes, opts)
if (!svg) return undefined
const fragment = new DocumentFragment()
const root = createRoot(fragment)
flushSync(() => root.render(svg.jsx))
const rendered = fragment.firstElementChild
root.unmount()
return rendered as SVGSVGElement
const result = await this.getSvgElement(shapes, opts)
if (!result) return undefined
return result.svg
}
/* --------------------- Events --------------------- */

Wyświetl plik

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

Wyświetl plik

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