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:
parent
84dbf2df20
commit
a18525ea78
5 changed files with 153 additions and 16 deletions
|
@ -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;
|
||||||
|
|
|
@ -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)",
|
||||||
|
|
|
@ -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 --------------------- */
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue