diff --git a/apps/www/components/Editor.tsx b/apps/www/components/Editor.tsx index c54887113..f5727a129 100644 --- a/apps/www/components/Editor.tsx +++ b/apps/www/components/Editor.tsx @@ -1,7 +1,8 @@ import React from 'react' import * as gtag from 'utils/gtag' -import { Tldraw, TldrawApp, useFileSystem } from '@tldraw/tldraw' +import { Tldraw, TldrawApp, TldrawProps, useFileSystem } from '@tldraw/tldraw' import { useAccountHandlers } from 'hooks/useAccountHandlers' +import { exportToImage } from 'utils/export' declare const window: Window & { app: TldrawApp } @@ -11,7 +12,12 @@ interface EditorProps { isSponsor?: boolean } -export default function Editor({ id = 'home', isUser = false, isSponsor = false }: EditorProps) { +export default function Editor({ + id = 'home', + isUser = false, + isSponsor = false, + ...rest +}: EditorProps & Partial) { const handleMount = React.useCallback((app: TldrawApp) => { window.app = app }, []) @@ -40,7 +46,9 @@ export default function Editor({ id = 'home', isUser = false, isSponsor = false showSponsorLink={!isSponsor} onSignIn={isSponsor ? undefined : onSignIn} onSignOut={isUser ? onSignOut : undefined} + onExport={exportToImage} {...fileSystemEvents} + {...rest} /> ) diff --git a/apps/www/components/MultiplayerEditor.tsx b/apps/www/components/MultiplayerEditor.tsx index b9e6792d0..cab6e8c77 100644 --- a/apps/www/components/MultiplayerEditor.tsx +++ b/apps/www/components/MultiplayerEditor.tsx @@ -6,6 +6,7 @@ import { LiveblocksProvider, RoomProvider } from '@liveblocks/react' import { useAccountHandlers } from 'hooks/useAccountHandlers' import { styled } from 'styles' import { useMultiplayerState } from 'hooks/useMultiplayerState' +import { exportToImage } from 'utils/export' import { useMultiplayerAssets } from 'hooks/useMultiplayerAssets' const client = createClient({ @@ -58,6 +59,7 @@ function Editor({ showSponsorLink={!isSponsor} onSignIn={isSponsor ? undefined : onSignIn} onSignOut={isUser ? onSignOut : undefined} + onExport={exportToImage} onAssetCreate={onAssetCreate} onAssetDelete={onAssetDelete} {...fileSystemEvents} diff --git a/apps/www/pages/api/export.ts b/apps/www/pages/api/export.ts new file mode 100644 index 000000000..8b9166fef --- /dev/null +++ b/apps/www/pages/api/export.ts @@ -0,0 +1,73 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import puppeteer from 'puppeteer' +import Cors from 'cors' +import { TDExport, TDExportTypes, TldrawApp } from '@tldraw/tldraw' + +const cors = Cors({ + methods: ['POST'], +}) + +function runMiddleware(req, res, fn) { + return new Promise((resolve, reject) => { + fn(req, res, (result) => { + if (result instanceof Error) return reject(result) + return resolve(result) + }) + }) +} + +const FRONTEND_URL = + process.env.NODE_ENV === 'development' + ? 'http://localhost:3000/?exportMode' + : 'https://www.tldraw.com/?exportMode' + +declare global { + interface Window { + app: TldrawApp + } +} + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + await runMiddleware(req, res, cors) + const { body } = req + const { + size: [width, height], + type, + } = body + if (type === TDExportTypes.PDF) res.status(500).send('Not implemented yet.') + let browser: puppeteer.Browser = null + try { + browser = await puppeteer.launch({ + slowMo: 50, + ignoreHTTPSErrors: true, + headless: true, + }) + const page = await browser.newPage() + await page.setUserAgent( + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36' + ) + await page.setViewport({ width: Math.floor(width), height: Math.floor(height) }) + await page.goto(FRONTEND_URL, { timeout: 15 * 1000, waitUntil: 'networkidle0' }) + await page.evaluateHandle('document.fonts.ready') + await page.evaluate(async (body: TDExport) => { + let app = window.app + if (!app) app = await new Promise((resolve) => setTimeout(() => resolve(window.app), 250)) + await app.ready + const { assets, shapes } = body + app.patchAssets(assets) + app.createShapes(...shapes) + app.selectAll() + app.zoomToSelection() + app.selectNone() + }, body) + const imageBuffer = await page.screenshot({ + type, + }) + res.status(200).send(imageBuffer) + } catch (err) { + console.error(err.message) + res.status(500).send(err) + } finally { + await browser.close() + } +} diff --git a/apps/www/pages/index.tsx b/apps/www/pages/index.tsx index 94a6f25fc..ecc311255 100644 --- a/apps/www/pages/index.tsx +++ b/apps/www/pages/index.tsx @@ -2,6 +2,8 @@ import dynamic from 'next/dynamic' import type { GetServerSideProps } from 'next' import { getSession } from 'next-auth/react' import Head from 'next/head' +import { useRouter } from 'next/router' +import { useMemo } from 'react' const Editor = dynamic(() => import('components/Editor'), { ssr: false }) @@ -11,12 +13,15 @@ interface PageProps { } export default function Home({ isUser, isSponsor }: PageProps): JSX.Element { + const { query } = useRouter() + const isExportMode = useMemo(() => 'exportMode' in query, [query]) + return ( <> tldraw - + ) } @@ -27,7 +32,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => { return { props: { isUser: session?.user ? true : false, - isSponsor: session?.isSponsor, + isSponsor: session?.isSponsor || false, }, } } diff --git a/apps/www/utils/export.ts b/apps/www/utils/export.ts new file mode 100644 index 000000000..698d7c71d --- /dev/null +++ b/apps/www/utils/export.ts @@ -0,0 +1,29 @@ +import { TDExport, TDExportTypes } from '@tldraw/tldraw' + +export const EXPORT_ENDPOINT = + process.env.NODE_ENV === 'development' + ? 'http://localhost:3000/api/export' + : 'https://www.tldraw.com/api/export' + +export async function exportToImage(info: TDExport) { + if (info.serialized) { + const link = document.createElement('a') + link.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(info.serialized) + link.download = info.name + '.' + info.type + link.click() + + return + } + + const response = await fetch(EXPORT_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(info), + }) + const blob = await response.blob() + const blobUrl = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = blobUrl + link.download = info.name + '.' + info.type + link.click() +} diff --git a/examples/core-example-advanced/src/styles.css b/examples/core-example-advanced/src/styles.css index c04208569..c32fdc97f 100644 --- a/examples/core-example-advanced/src/styles.css +++ b/examples/core-example-advanced/src/styles.css @@ -1,4 +1,4 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700'); html, * { diff --git a/examples/tldraw-example/src/export.tsx b/examples/tldraw-example/src/export.tsx new file mode 100644 index 000000000..c6d9c8050 --- /dev/null +++ b/examples/tldraw-example/src/export.tsx @@ -0,0 +1,33 @@ +import * as React from 'react' +import { TDExport, Tldraw } from '@tldraw/tldraw' + +export default function Export(): JSX.Element { + const handleExport = React.useCallback(async (info: TDExport) => { + if (info.serialized) { + const link = document.createElement('a') + link.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(info.serialized) + link.download = info.name + '.' + info.type + link.click() + + return + } + + const response = await fetch('some_serverless_endpoint', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(info), + }) + const blob = await response.blob() + const blobUrl = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = blobUrl + link.download = info.name + '.' + info.type + link.click() + }, []) + + return ( +
+ +
+ ) +} diff --git a/packages/tldraw/package.json b/packages/tldraw/package.json index 2a75353f3..7b7b7c960 100644 --- a/packages/tldraw/package.json +++ b/packages/tldraw/package.json @@ -53,6 +53,7 @@ "@tldraw/vec": "^1.4.3", "idb-keyval": "^6.0.3", "perfect-freehand": "^1.0.16", + "puppeteer": "^13.0.1", "react-hotkeys-hook": "^3.4.0", "tslib": "^2.3.1", "zustand": "^3.6.5" diff --git a/packages/tldraw/src/Tldraw.tsx b/packages/tldraw/src/Tldraw.tsx index ed828d008..44689d4d1 100644 --- a/packages/tldraw/src/Tldraw.tsx +++ b/packages/tldraw/src/Tldraw.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { Renderer } from '@tldraw/core' import { styled, dark } from '~styles' -import { TDDocument, TDShape, TDBinding, TDStatus, TDUser, TDAsset } from '~types' +import { TDDocument, TDStatus } from '~types' import { TldrawApp, TDCallbacks } from '~state' import { TldrawContext, useStylesheet, useKeyboardShortcuts, useTldrawApp } from '~hooks' import { shapeUtils } from '~state/shapes' @@ -86,85 +86,6 @@ export interface TldrawProps extends TDCallbacks { * bucket based solution will cause massive base64 string to be written to the liveblocks room. */ disableAssets?: boolean - - /** - * (optional) A callback to run when the component mounts. - */ - onMount?: (state: TldrawApp) => void - - /** - * (optional) A callback to run when the user creates a new project through the menu or through a keyboard shortcut. - */ - onNewProject?: (state: TldrawApp, e?: KeyboardEvent) => void - - /** - * (optional) A callback to run when the user saves a project through the menu or through a keyboard shortcut. - */ - onSaveProject?: (state: TldrawApp, e?: KeyboardEvent) => void - - /** - * (optional) A callback to run when the user saves a project as a new project through the menu or through a keyboard shortcut. - */ - onSaveProjectAs?: (state: TldrawApp, e?: KeyboardEvent) => void - - /** - * (optional) A callback to run when the user opens new project through the menu or through a keyboard shortcut. - */ - onOpenProject?: (state: TldrawApp, e?: KeyboardEvent) => void - - /** - * (optional) A callback to run when the user signs in via the menu. - */ - onSignIn?: (state: TldrawApp) => void - - /** - * (optional) A callback to run when the user signs out via the menu. - */ - onSignOut?: (state: TldrawApp) => void - - /** - * (optional) A callback to run when the user creates a new project. - */ - onChangePresence?: (state: TldrawApp, user: TDUser) => void - /** - * (optional) A callback to run when the component's state changes. - */ - onChange?: (state: TldrawApp, reason?: string) => void - /** - * (optional) A callback to run when the state is patched. - */ - onPatch?: (state: TldrawApp, reason?: string) => void - /** - * (optional) A callback to run when the state is changed with a command. - */ - onCommand?: (state: TldrawApp, reason?: string) => void - /** - * (optional) A callback to run when the state is persisted. - */ - onPersist?: (state: TldrawApp) => void - /** - * (optional) A callback to run when the user undos. - */ - onUndo?: (state: TldrawApp) => void - /** - * (optional) A callback to run when the user redos. - */ - onRedo?: (state: TldrawApp) => void - /** - * (optional) A callback to run when an asset will be deleted. - */ - onAssetDelete?: (assetId: string) => void - /** - * (optional) A callback to run when an asset will be created. Should return the value for the image/video's `src` property. - */ - onAssetCreate?: (file: File, id: string) => Promise - - onChangePage?: ( - app: TldrawApp, - shapes: Record, - bindings: Record, - assets: Record - ) => void } export function Tldraw({ @@ -199,6 +120,7 @@ export function Tldraw({ onChangePage, onAssetCreate, onAssetDelete, + onExport, }: TldrawProps) { const [sId, setSId] = React.useState(id) @@ -250,6 +172,7 @@ export function Tldraw({ onChangePage, onAssetDelete, onAssetCreate, + onExport, }) setSId(id) setApp(newApp) @@ -303,6 +226,7 @@ export function Tldraw({ onChangePage, onAssetDelete, onAssetCreate, + onExport, } }, [ onMount, @@ -323,6 +247,7 @@ export function Tldraw({ onChangePage, onAssetDelete, onAssetCreate, + onExport, ]) // Use the `key` to ensure that new selector hooks are made when the id changes diff --git a/packages/tldraw/src/components/ContextMenu/ContextMenu.tsx b/packages/tldraw/src/components/ContextMenu/ContextMenu.tsx index b53c68f73..4ffd3e5db 100644 --- a/packages/tldraw/src/components/ContextMenu/ContextMenu.tsx +++ b/packages/tldraw/src/components/ContextMenu/ContextMenu.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { styled } from '~styles' import * as RadixContextMenu from '@radix-ui/react-context-menu' import { useTldrawApp } from '~hooks' -import { TDSnapshot, AlignType, DistributeType, StretchType } from '~types' +import { TDSnapshot, AlignType, DistributeType, StretchType, TDExportTypes } from '~types' import { AlignBottomIcon, AlignCenterHorizontallyIcon, @@ -130,6 +130,26 @@ const InnerMenu = React.memo(function InnerMenu({ onBlur }: InnerContextMenuProp app.redo() }, [app]) + const handleExportPNG = React.useCallback(async () => { + await app.exportSelectedShapesAs(TDExportTypes.PNG) + }, [app]) + + const handleExportJPG = React.useCallback(async () => { + await app.exportSelectedShapesAs(TDExportTypes.JPG) + }, [app]) + + const handleExportWEBP = React.useCallback(async () => { + await app.exportSelectedShapesAs(TDExportTypes.WEBP) + }, [app]) + + const handleExportSVG = React.useCallback(async () => { + await app.exportSelectedShapesAs(TDExportTypes.SVG) + }, [app]) + + const handleExportJSON = React.useCallback(async () => { + await app.exportSelectedShapesAs(TDExportTypes.JSON) + }, [app]) + const hasSelection = numberOfSelectedIds > 0 const hasTwoOrMore = numberOfSelectedIds > 1 const hasThreeOrMore = numberOfSelectedIds > 2 @@ -188,6 +208,31 @@ const InnerMenu = React.memo(function InnerMenu({ onBlur }: InnerContextMenuProp {hasTwoOrMore && ( )} + {app.callbacks.onExport ? ( + <> + + + PNG + JPG + WEBP + SVG + JSON + + + Copy as SVG + + {isDebugMode && Copy as JSON} + + + ) : ( + <> + + + Copy as SVG + + {isDebugMode && Copy as JSON} + + )} Cut @@ -195,13 +240,10 @@ const InnerMenu = React.memo(function InnerMenu({ onBlur }: InnerContextMenuProp Copy - - Copy as SVG - - {isDebugMode && Copy as JSON} Paste + Delete @@ -376,15 +418,20 @@ function MoveToPageMenu(): JSX.Element | null { export interface ContextMenuSubMenuProps { label: string + size?: 'small' children: React.ReactNode } -export function ContextMenuSubMenu({ children, label }: ContextMenuSubMenuProps): JSX.Element { +export function ContextMenuSubMenu({ + children, + label, + size, +}: ContextMenuSubMenuProps): JSX.Element { return ( {label} - + {children} diff --git a/packages/tldraw/src/components/Loading/Loading.tsx b/packages/tldraw/src/components/Loading/Loading.tsx index 7c7f66ac1..97d2b8430 100644 --- a/packages/tldraw/src/components/Loading/Loading.tsx +++ b/packages/tldraw/src/components/Loading/Loading.tsx @@ -20,7 +20,7 @@ const StyledLoadingPanelContainer = styled('div', { transform: `translate(-50%, 0)`, borderBottomLeftRadius: '12px', borderBottomRightRadius: '12px', - padding: '8px', + padding: '8px 16px', fontFamily: 'var(--fonts-ui)', fontSize: 'var(--fontSizes-1)', boxShadow: 'var(--shadows-panel)', diff --git a/packages/tldraw/src/components/Primitives/DropdownMenu/DMSubMenu.tsx b/packages/tldraw/src/components/Primitives/DropdownMenu/DMSubMenu.tsx index 64eada1e4..4d49e9e0a 100644 --- a/packages/tldraw/src/components/Primitives/DropdownMenu/DMSubMenu.tsx +++ b/packages/tldraw/src/components/Primitives/DropdownMenu/DMSubMenu.tsx @@ -5,11 +5,17 @@ import { MenuContent } from '~components/Primitives/MenuContent' export interface DMSubMenuProps { label: string + size?: 'small' disabled?: boolean children: React.ReactNode } -export function DMSubMenu({ children, disabled = false, label }: DMSubMenuProps): JSX.Element { +export function DMSubMenu({ + children, + size, + disabled = false, + label, +}: DMSubMenuProps): JSX.Element { return ( @@ -18,7 +24,7 @@ export function DMSubMenu({ children, disabled = false, label }: DMSubMenuProps) - + {children} diff --git a/packages/tldraw/src/components/Primitives/MenuContent/MenuContent.ts b/packages/tldraw/src/components/Primitives/MenuContent/MenuContent.ts index bacd4aa62..3962faf04 100644 --- a/packages/tldraw/src/components/Primitives/MenuContent/MenuContent.ts +++ b/packages/tldraw/src/components/Primitives/MenuContent/MenuContent.ts @@ -14,4 +14,11 @@ export const MenuContent = styled('div', { padding: '$2 $2', borderRadius: '$3', font: '$ui', + variants: { + size: { + small: { + minWidth: 72, + }, + }, + }, }) diff --git a/packages/tldraw/src/components/TopPanel/Menu/Menu.tsx b/packages/tldraw/src/components/TopPanel/Menu/Menu.tsx index ffc9bf5a4..46e3cd999 100644 --- a/packages/tldraw/src/components/TopPanel/Menu/Menu.tsx +++ b/packages/tldraw/src/components/TopPanel/Menu/Menu.tsx @@ -15,7 +15,7 @@ import { useFileSystemHandlers } from '~hooks' import { HeartIcon } from '~components/Primitives/icons/HeartIcon' import { preventEvent } from '~components/preventEvent' import { DiscordIcon } from '~components/Primitives/icons' -import type { TDSnapshot } from '~types' +import { TDExportTypes, TDSnapshot } from '~types' import { Divider } from '~components/Primitives/Divider' interface MenuProps { @@ -33,11 +33,41 @@ const disableAssetsSelector = (s: TDSnapshot) => { export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: MenuProps) { const app = useTldrawApp() + const numberOfSelectedIds = app.useStore(numberOfSelectedIdsSelector) + const disableAssets = app.useStore(disableAssetsSelector) + const [_, setForce] = React.useState(0) + + React.useEffect(() => setForce(1), []) + const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs } = useFileSystemHandlers() + const handleExportPNG = React.useCallback(async () => { + await app.exportAllShapesAs(TDExportTypes.PNG) + }, [app]) + + const handleExportJPG = React.useCallback(async () => { + await app.exportAllShapesAs(TDExportTypes.JPG) + }, [app]) + + const handleExportWEBP = React.useCallback(async () => { + await app.exportAllShapesAs(TDExportTypes.WEBP) + }, [app]) + + const handleExportPDF = React.useCallback(async () => { + await app.exportAllShapesAs(TDExportTypes.PDF) + }, [app]) + + const handleExportSVG = React.useCallback(async () => { + await app.exportAllShapesAs(TDExportTypes.SVG) + }, [app]) + + const handleExportJSON = React.useCallback(async () => { + await app.exportAllShapesAs(TDExportTypes.JSON) + }, [app]) + const handleSignIn = React.useCallback(() => { app.callbacks.onSignIn?.(app) }, [app]) @@ -82,7 +112,8 @@ export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: Menu app.callbacks.onNewProject || app.callbacks.onOpenProject || app.callbacks.onSaveProject || - app.callbacks.onSaveProjectAs + app.callbacks.onSaveProjectAs || + app.callbacks.onExport const showSignInOutMenu = app.callbacks.onSignIn || app.callbacks.onSignOut || showSponsorLink @@ -116,6 +147,18 @@ export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: Menu Save As... )} + {app.callbacks.onExport && ( + <> + + + PNG + JPG + WEBP + SVG + JSON + + + )} {!disableAssets && ( <> diff --git a/packages/tldraw/src/constants.ts b/packages/tldraw/src/constants.ts index 2612d3685..f500f66aa 100644 --- a/packages/tldraw/src/constants.ts +++ b/packages/tldraw/src/constants.ts @@ -13,7 +13,6 @@ export const VERY_SLOW_SPEED = 2.5 export const GHOSTED_OPACITY = 0.3 export const DEAD_ZONE = 3 export const LABEL_POINT = [0.5, 0.5] - import type { Easing } from '~types' export const PI2 = Math.PI * 2 @@ -84,7 +83,7 @@ export const USER_COLORS = [ '#FF802B', ] -const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) - +export const isSafari = + typeof Window === 'undefined' ? false : /^((?!chrome|android).)*safari/i.test(navigator.userAgent) export const IMAGE_EXTENSIONS = ['.png', '.svg', '.jpg', '.jpeg', '.gif'] export const VIDEO_EXTENSIONS = isSafari ? [] : ['.mp4', '.webm'] diff --git a/packages/tldraw/src/hooks/useStylesheet.ts b/packages/tldraw/src/hooks/useStylesheet.ts index 36e11073c..c3bb74016 100644 --- a/packages/tldraw/src/hooks/useStylesheet.ts +++ b/packages/tldraw/src/hooks/useStylesheet.ts @@ -10,13 +10,11 @@ const CSS = ` export function useStylesheet() { React.useLayoutEffect(() => { if (styles.get(UID)) return - const style = document.createElement('style') style.innerHTML = CSS style.setAttribute('id', UID) document.head.appendChild(style) styles.set(UID, style) - return () => { if (style && document.head.contains(style)) { document.head.removeChild(style) diff --git a/packages/tldraw/src/hooks/useTldrawApp.tsx b/packages/tldraw/src/hooks/useTldrawApp.tsx index 92b5f9872..52fe3ceaf 100644 --- a/packages/tldraw/src/hooks/useTldrawApp.tsx +++ b/packages/tldraw/src/hooks/useTldrawApp.tsx @@ -5,6 +5,5 @@ export const TldrawContext = React.createContext({} as TldrawApp) export function useTldrawApp() { const context = React.useContext(TldrawContext) - return context } diff --git a/packages/tldraw/src/state/TldrawApp.ts b/packages/tldraw/src/state/TldrawApp.ts index fd25b8fa9..6d118f5ba 100644 --- a/packages/tldraw/src/state/TldrawApp.ts +++ b/packages/tldraw/src/state/TldrawApp.ts @@ -37,6 +37,10 @@ import { TDToolType, TDAssetType, TDAsset, + TDExportTypes, + TDAssets, + TDExport, + ImageShape, } from '~types' import { migrate, @@ -53,7 +57,6 @@ import { shapeUtils } from '~state/shapes' import { defaultStyle } from '~state/shapes/shared/shape-styles' import * as Commands from './commands' import { SessionArgsOfType, getSession, TldrawSession } from './sessions' -import type { BaseTool } from './tools/BaseTool' import { USER_COLORS, FIT_TO_SCREEN_PADDING, @@ -62,6 +65,7 @@ import { VIDEO_EXTENSIONS, SVG_EXPORT_PADDING, } from '~constants' +import type { BaseTool } from './tools/BaseTool' import { SelectTool } from './tools/SelectTool' import { EraseTool } from './tools/EraseTool' import { TextTool } from './tools/TextTool' @@ -154,6 +158,10 @@ export interface TDCallbacks { * (optional) A callback to run when an asset will be created. Should return the value for the image/video's `src` property. */ onAssetCreate?: (file: File, id: string) => Promise + /** + * (optional) A callback to run when the user exports their page or selection. + */ + onExport?: (info: TDExport) => Promise } export class TldrawApp extends StateManager { @@ -438,6 +446,8 @@ export class TldrawApp extends StateManager { } // Cleanup assets + if (!('assets' in next.document)) next.document.assets = {} + Object.keys(next.document.assets).forEach((id) => { if (!next.document.assets[id]) { delete next.document.assets[id] @@ -1689,6 +1699,7 @@ export class TldrawApp extends StateManager { const copyingAssets = copyingShapes .map((shape) => { if (!shape.assetId) return + return this.document.assets[shape.assetId] }) .filter(Boolean) as TDAsset[] @@ -1878,9 +1889,12 @@ export class TldrawApp extends StateManager { const bounds = util.getBounds(shape) const elm = util.getSvgElement(shape) if (!elm) return + // If the element is an image, set the asset src as the xlinkhref if (shape.type === TDShapeType.Image) { elm.setAttribute('xlink:href', this.document.assets[shape.assetId].src) + } else if (shape.type === TDShapeType.Video) { + elm.setAttribute('xlink:href', this.serializeVideo(shape.id)) } // Put the element in the correct position relative to the common bounds elm.setAttribute( @@ -2049,29 +2063,22 @@ export class TldrawApp extends StateManager { * Zoom to fit the page's shapes. */ zoomToFit = (): this => { - const shapes = this.shapes - + const { + shapes, + pageState: { camera }, + } = this if (shapes.length === 0) return this - const { rendererBounds } = this - const commonBounds = Utils.getCommonBounds(shapes.map(TLDR.getBounds)) - let zoom = TLDR.getCameraZoom( Math.min( (rendererBounds.width - FIT_TO_SCREEN_PADDING) / commonBounds.width, (rendererBounds.height - FIT_TO_SCREEN_PADDING) / commonBounds.height ) ) - - zoom = - this.pageState.camera.zoom === zoom || this.pageState.camera.zoom < 1 - ? Math.min(1, zoom) - : zoom - + zoom = camera.zoom === zoom || camera.zoom < 1 ? Math.min(1, zoom) : zoom const mx = (rendererBounds.width - commonBounds.width * zoom) / 2 / zoom const my = (rendererBounds.height - commonBounds.height * zoom) / 2 / zoom - return this.setCamera( Vec.toFixed(Vec.sub([mx, my], [commonBounds.minX, commonBounds.minY])), zoom, @@ -3388,6 +3395,100 @@ export class TldrawApp extends StateManager { return this.selectedIds.includes(id) } + /* ----------------- Export ----------------- */ + + /** + * Get a snapshot of a video at current frame as base64 encoded image + * @param id ID of video shape + * @returns base64 encoded frame + * @throws Error if video shape with given ID does not exist + */ + serializeVideo(id: string): string { + const video = document.getElementById(id + '_video') as HTMLVideoElement + if (video) { + const canvas = document.createElement('canvas') + canvas.width = video.videoWidth + canvas.height = video.videoHeight + const canvasContext = canvas.getContext('2d')! + canvasContext.drawImage(video, 0, 0) + return canvas.toDataURL('image/png') + } else throw new Error('Video with id ' + id + ' not found') + } + + patchAssets(assets: TDAssets) { + this.document.assets = { + ...this.document.assets, + ...assets, + } + } + + async exportAllShapesAs(type: TDExportTypes) { + const initialSelectedIds = [...this.selectedIds] + this.selectAll() + const { width, height } = Utils.expandBounds(TLDR.getSelectedBounds(this.state), 64) + const allIds = [...this.selectedIds] + this.setSelectedIds(initialSelectedIds) + await this.exportShapesAs(allIds, [width, height], type) + } + + async exportSelectedShapesAs(type: TDExportTypes) { + const { width, height } = Utils.expandBounds(TLDR.getSelectedBounds(this.state), 64) + await this.exportShapesAs(this.selectedIds, [width, height], type) + } + + async exportShapesAs(shapeIds: string[], size: number[], type: TDExportTypes) { + this.setIsLoading(true) + const assets: TDAssets = {} + let shapes = shapeIds.map((id) => ({ ...this.getShape(id) })) + + // Patch asset table. Replace videos with serialized snapshots + shapes.forEach((s, i) => { + if (s.assetId) { + assets[s.assetId] = { ...this.document.assets[s.assetId] } + if (s.type === TDShapeType.Video) { + assets[s.assetId].src = this.serializeVideo(s.id) + assets[s.assetId].type = TDAssetType.Image + } + } + }) + + // Cast exported video shapes to image shapes to properly display snapshots + shapes = shapes.map((s) => { + if (s.type === TDShapeType.Video) { + const shape = s as TDShape + shape.type = TDShapeType.Image + return shape as ImageShape + } else return s + }) + + let serializedExport + if (type == TDExportTypes.SVG) { + serializedExport = this.copySvg(shapeIds) + } else if (type == TDExportTypes.JSON) { + serializedExport = this.copyJson(shapeIds) + } + + const exportInfo: TDExport = { + name: this.page.name ?? 'export', + shapes: shapes, + assets: assets, + type, + size: type === 'png' ? Vec.mul(size, 2) : size, + serialized: serializedExport, + } + + if (this.callbacks.onExport) { + try { + this.setIsLoading(true) + await this.callbacks.onExport?.(exportInfo) + } catch (error) { + console.error(error) + } finally { + this.setIsLoading(false) + } + } + } + get room() { return this.state.room } diff --git a/packages/tldraw/src/state/data/filesystem.ts b/packages/tldraw/src/state/data/filesystem.ts index bc1b2ff1a..ea8e7cb4a 100644 --- a/packages/tldraw/src/state/data/filesystem.ts +++ b/packages/tldraw/src/state/data/filesystem.ts @@ -1,5 +1,6 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ import type { TDDocument, TDFile } from '~types' -import { fileSave, fileOpen, FileSystemHandle } from './browser-fs-access' +import type { FileSystemHandle } from './browser-fs-access' import { get as getFromIdb, set as setToIdb } from 'idb-keyval' import { IMAGE_EXTENSIONS, VIDEO_EXTENSIONS } from '~constants' @@ -46,6 +47,8 @@ export async function saveToFileSystem(document: TDDocument, fileHandle: FileSys } // Save to file system + // @ts-ignore + const fileSave = await import('./browser-fs-access').default.fileSave const newFileHandle = await fileSave( blob, { @@ -67,6 +70,8 @@ export async function openFromFileSystem(): Promise { // Get the blob + // @ts-ignore + const fileOpen = await import('./browser-fs-access').fileOpen const blob = await fileOpen({ description: 'Tldraw File', extensions: [`.tldr`], @@ -100,6 +105,8 @@ export async function openFromFileSystem(): Promise { const elm = document.createElementNS('http://www.w3.org/2000/svg', 'image') elm.setAttribute('width', `${bounds.width}`) elm.setAttribute('height', `${bounds.height}`) + elm.setAttribute('xmlns:xlink', `http://www.w3.org/1999/xlink`) return elm } } diff --git a/packages/tldraw/src/state/shapes/VideoUtil/VideoUtil.tsx b/packages/tldraw/src/state/shapes/VideoUtil/VideoUtil.tsx index ab0afbf23..9657f599e 100644 --- a/packages/tldraw/src/state/shapes/VideoUtil/VideoUtil.tsx +++ b/packages/tldraw/src/state/shapes/VideoUtil/VideoUtil.tsx @@ -172,6 +172,15 @@ export class VideoUtil extends TDShapeUtil { return next.size !== prev.size || next.style !== prev.style || next.isPlaying !== prev.isPlaying } + getSvgElement = (shape: VideoShape) => { + const bounds = this.getBounds(shape) + const elm = document.createElementNS('http://www.w3.org/2000/svg', 'image') + elm.setAttribute('width', `${bounds.width}`) + elm.setAttribute('height', `${bounds.height}`) + elm.setAttribute('xmlns:xlink', `http://www.w3.org/1999/xlink`) + return elm + } + transform = transformRectangle transformSingle = transformSingleRectangle diff --git a/packages/tldraw/src/types.ts b/packages/tldraw/src/types.ts index dc2a24acb..c6cc27052 100644 --- a/packages/tldraw/src/types.ts +++ b/packages/tldraw/src/types.ts @@ -20,6 +20,7 @@ import type { TLShapeBlurHandler, TLShapeCloneHandler, TLAsset, + TLBounds, } from '@tldraw/core' /* -------------------------------------------------- */ @@ -293,6 +294,7 @@ export enum Decoration { export interface TDBaseShape extends TLShape { style: ShapeStyles type: TDShapeType + label?: string handles?: Record } @@ -484,6 +486,28 @@ export type TDAsset = TDImageAsset | TDVideoAsset export type TDAssets = Record +/* -------------------------------------------------- */ +/* Export */ +/* -------------------------------------------------- */ + +export enum TDExportTypes { + PNG = 'png', + JPG = 'jpeg', + WEBP = 'webp', + PDF = 'pdf', + SVG = 'svg', + JSON = 'json', +} + +export interface TDExport { + name: string + shapes: TDShape[] + assets: TDAssets + type: TDExportTypes + size: number[] + serialized?: string +} + /* -------------------------------------------------- */ /* Type Helpers */ /* -------------------------------------------------- */ diff --git a/yarn.lock b/yarn.lock index 936c62dd4..e2f554364 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4493,21 +4493,6 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== -aws-sdk@^2.1053.0: - version "2.1053.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1053.0.tgz#cd8263bde89177351e7ef49d9e6a89d4584c852c" - integrity sha512-bsVudymGczfn7kOsY9tiMFZUCNFOQi7iG3d1HiBFrnEDCKtVTyKuFrXy4iKUPCcjfOaqNnb1S3ZxN/A70MOTkg== - dependencies: - buffer "4.9.2" - events "1.1.1" - ieee754 "1.1.13" - jmespath "0.15.0" - querystring "0.2.0" - sax "1.2.1" - url "0.10.3" - uuid "3.3.2" - xml2js "0.4.19" - aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -4937,15 +4922,6 @@ buffer-xor@^1.0.3: resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= -buffer@4.9.2: - version "4.9.2" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" - integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - buffer@5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" @@ -6426,6 +6402,11 @@ detective-typescript-70@^7.0.0: node-source-walk "^4.2.0" typescript "^3.9.7" +devtools-protocol@0.0.937139: + version "0.0.937139" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.937139.tgz#bdee3751fdfdb81cb701fd3afa94b1065dafafcf" + integrity sha512-daj+rzR3QSxsPRy5vjjthn58axO8c11j58uY0lG5vvlJk/EiOdCWOptGdkXDjtuRHr78emKq0udHCXM4trhoDQ== + dezalgo@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" @@ -7313,11 +7294,6 @@ eventemitter3@^4.0.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== -events@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" - integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= - events@3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -7450,17 +7426,7 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extract-zip@^1.0.3: - version "1.7.0" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" - integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== - dependencies: - concat-stream "^1.6.2" - debug "^2.6.9" - mkdirp "^0.5.4" - yauzl "^2.10.0" - -extract-zip@^2.0.1: +extract-zip@2.0.1, extract-zip@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== @@ -7471,6 +7437,16 @@ extract-zip@^2.0.1: optionalDependencies: "@types/yauzl" "^2.9.1" +extract-zip@^1.0.3: + version "1.7.0" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" + integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== + dependencies: + concat-stream "^1.6.2" + debug "^2.6.9" + mkdirp "^0.5.4" + yauzl "^2.10.0" + extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" @@ -8571,6 +8547,14 @@ https-browserify@1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= +https-proxy-agent@5.0.0, https-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + dependencies: + agent-base "6" + debug "4" + https-proxy-agent@^2.2.3: version "2.2.4" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" @@ -8579,14 +8563,6 @@ https-proxy-agent@^2.2.3: agent-base "^4.3.0" debug "^3.1.0" -https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== - dependencies: - agent-base "6" - debug "4" - human-signals@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" @@ -8648,11 +8624,6 @@ idb@^6.1.4: resolved "https://registry.yarnpkg.com/idb/-/idb-6.1.5.tgz#dbc53e7adf1ac7c59f9b2bf56e00b4ea4fce8c7b" integrity sha512-IJtugpKkiVXQn5Y+LteyBCNk1N8xpGV3wWZk9EVtZWH8DYkjBn0bX1XnGP9RkyZF0sAcywa6unHqSWKe7q4LGw== -ieee754@1.1.13: - version "1.1.13" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" - integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== - ieee754@^1.1.13, ieee754@^1.1.4: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -9280,7 +9251,7 @@ isarray@0.0.1: resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= -isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: +isarray@1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= @@ -9793,11 +9764,6 @@ jest@^27.3.1: import-local "^3.0.2" jest-cli "^27.4.7" -jmespath@0.15.0: - version "0.15.0" - resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" - integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc= - joi@^17.4.2: version "17.5.0" resolved "https://registry.yarnpkg.com/joi/-/joi-17.5.0.tgz#7e66d0004b5045d971cf416a55fb61d33ac6e011" @@ -12089,6 +12055,13 @@ pirates@^4.0.4: resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.4.tgz#07df81e61028e402735cdd49db701e4885b4e6e6" integrity sha512-ZIrVPH+A52Dw84R0L3/VS9Op04PuQ2SEoJL6bkshmiTic/HldyW9Tf7oH5mhJZBK7NmDx27vSMrYEXPXclpDKw== +pkg-dir@4.2.0, pkg-dir@^4.1.0, pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + pkg-dir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" @@ -12096,13 +12069,6 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" -pkg-dir@^4.1.0, pkg-dir@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - platform@1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7" @@ -12300,7 +12266,7 @@ process@0.11.10, process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= -progress@^2.0.0, progress@^2.0.3: +progress@2.0.3, progress@^2.0.0, progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== @@ -12392,7 +12358,7 @@ protoduck@^5.0.1: dependencies: genfun "^5.0.0" -proxy-from-env@^1.1.0: +proxy-from-env@1.1.0, proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== @@ -12439,11 +12405,6 @@ pumpify@^1.3.3: inherits "^2.0.3" pump "^2.0.0" -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -12456,6 +12417,24 @@ pupa@^2.1.1: dependencies: escape-goat "^2.0.0" +puppeteer@^13.0.1: + version "13.0.1" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-13.0.1.tgz#9cd9bb8ec090bade183ca186bf342396bdffa135" + integrity sha512-wqGIx59LzYqWhYcJQphMT+ux0sgatEUbjKG0lbjJxNVqVIT3ZC5m4Bvmq2gHE3qhb63EwS+rNkql08bm4BvO0A== + dependencies: + debug "4.3.2" + devtools-protocol "0.0.937139" + extract-zip "2.0.1" + https-proxy-agent "5.0.0" + node-fetch "2.6.5" + pkg-dir "4.2.0" + progress "2.0.3" + proxy-from-env "1.1.0" + rimraf "3.0.2" + tar-fs "2.1.1" + unbzip2-stream "1.4.3" + ws "8.2.3" + q@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -12488,11 +12467,6 @@ querystring-es3@0.2.1: resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -13256,11 +13230,6 @@ sass-lookup@^3.0.0: dependencies: commander "^2.16.0" -sax@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" - integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= - sax@>=0.6.0, sax@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -14159,7 +14128,7 @@ tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== -tar-fs@^2.0.0: +tar-fs@2.1.1, tar-fs@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== @@ -14739,7 +14708,7 @@ unbox-primitive@^1.0.1: has-symbols "^1.0.2" which-boxed-primitive "^1.0.2" -unbzip2-stream@^1.0.9: +unbzip2-stream@1.4.3, unbzip2-stream@^1.0.9: version "1.4.3" resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== @@ -14895,14 +14864,6 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" -url@0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" - integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ= - dependencies: - punycode "1.3.2" - querystring "0.2.0" - use-callback-ref@^1.2.3: version "1.2.5" resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.5.tgz#6115ed242cfbaed5915499c0a9842ca2912f38a5" @@ -14962,11 +14923,6 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid@3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" - integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== - uuid@^3.0.1, uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" @@ -15533,6 +15489,11 @@ write-pkg@^3.1.0: sort-keys "^2.0.0" write-json-file "^2.2.0" +ws@8.2.3: + version "8.2.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" + integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== + ws@>=7.4.6: version "8.4.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.4.0.tgz#f05e982a0a88c604080e8581576e2a063802bed6" @@ -15553,14 +15514,6 @@ xml-name-validator@^3.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== -xml2js@0.4.19: - version "0.4.19" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" - integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== - dependencies: - sax ">=0.6.0" - xmlbuilder "~9.0.1" - xml2js@^0.4.23: version "0.4.23" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" @@ -15574,7 +15527,7 @@ xmlbuilder@>=11.0.1: resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== -xmlbuilder@^9.0.7, xmlbuilder@~9.0.1: +xmlbuilder@^9.0.7: version "9.0.7" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=