kopia lustrzana https://github.com/Tldraw/Tldraw
use native structuredClone on node, cloudflare workers, and in tests (#3166)
Currently, we only use native `structuredClone` in the browser, falling back to `JSON.parse(JSON.stringify(...))` elsewhere, despite Node supporting `structuredClone` [since v17](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) and Cloudflare Workers supporting it [since 2022](https://blog.cloudflare.com/standards-compliant-workers-api/). This PR adjusts our shim to use the native `structuredClone` on all platforms, if available. Additionally, `jsdom` doesn't implement `structuredClone`, a bug [open since 2022](https://github.com/jsdom/jsdom/issues/3363). This PR patches `jsdom` environment in all packages/apps that use it for tests. Also includes a driveby removal of `deepCopy`, a function that is strictly inferior to `structuredClone`. ### Change Type <!-- ❗ Please select a 'Scope' label ❗️ --> - [x] `sdk` — Changes the tldraw SDK - [x] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff <!-- ❗ Please select a 'Type' label ❗️ --> - [ ] `bugfix` — Bug fix - [ ] `feature` — New feature - [x] `improvement` — Improving existing features - [x] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know ### Test Plan 1. A smoke test would be enough - [ ] Unit Tests - [x] End to end testspull/3147/head
rodzic
1951fc0e47
commit
d7b80baa31
|
@ -29,3 +29,5 @@ apps/vscode/extension/editor/tldraw-assets.json
|
||||||
**/coverage/**/*
|
**/coverage/**/*
|
||||||
|
|
||||||
apps/dotcom/public/sw.js
|
apps/dotcom/public/sw.js
|
||||||
|
|
||||||
|
patchedJestJsDom.js
|
|
@ -66,6 +66,10 @@ module.exports = {
|
||||||
message: 'Use the getFromSessionStorage/setInSessionStorage helpers instead',
|
message: 'Use the getFromSessionStorage/setInSessionStorage helpers instead',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
'no-restricted-globals': [
|
||||||
|
'error',
|
||||||
|
{ name: 'structuredClone', message: 'Use structuredClone from @tldraw/util instead' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
|
|
|
@ -38,15 +38,15 @@ jobs:
|
||||||
- name: Check for installation warnings
|
- name: Check for installation warnings
|
||||||
run: 'yarn | grep -vzq "with warnings"'
|
run: 'yarn | grep -vzq "with warnings"'
|
||||||
|
|
||||||
|
- name: Check tsconfigs
|
||||||
|
run: yarn check-tsconfigs
|
||||||
|
|
||||||
- name: Typecheck
|
- name: Typecheck
|
||||||
run: yarn build-types
|
run: yarn build-types
|
||||||
|
|
||||||
- name: Check scripts
|
- name: Check scripts
|
||||||
run: yarn check-scripts
|
run: yarn check-scripts
|
||||||
|
|
||||||
- name: Check tsconfigs
|
|
||||||
run: yarn check-tsconfigs
|
|
||||||
|
|
||||||
- name: Check PR template
|
- name: Check PR template
|
||||||
run: yarn update-pr-template --check
|
run: yarn update-pr-template --check
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { SearchResult } from '@/types/search-types'
|
import { SearchResult } from '@/types/search-types'
|
||||||
import { getDb } from '@/utils/ContentDatabase'
|
import { getDb } from '@/utils/ContentDatabase'
|
||||||
import { SEARCH_RESULTS, searchBucket, sectionTypeBucket } from '@/utils/search-api'
|
import { SEARCH_RESULTS, searchBucket, sectionTypeBucket } from '@/utils/search-api'
|
||||||
|
import { structuredClone } from '@tldraw/utils'
|
||||||
import assert from 'assert'
|
import assert from 'assert'
|
||||||
import { NextRequest } from 'next/server'
|
import { NextRequest } from 'next/server'
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { SearchResult } from '@/types/search-types'
|
import { SearchResult } from '@/types/search-types'
|
||||||
import { getDb } from '@/utils/ContentDatabase'
|
import { getDb } from '@/utils/ContentDatabase'
|
||||||
import { SEARCH_RESULTS, searchBucket, sectionTypeBucket } from '@/utils/search-api'
|
import { SEARCH_RESULTS, searchBucket, sectionTypeBucket } from '@/utils/search-api'
|
||||||
|
import { structuredClone } from '@tldraw/utils'
|
||||||
import { NextRequest } from 'next/server'
|
import { NextRequest } from 'next/server'
|
||||||
|
|
||||||
type Data = {
|
type Data = {
|
||||||
|
|
|
@ -49,6 +49,7 @@
|
||||||
"@microsoft/tsdoc": "^0.14.2",
|
"@microsoft/tsdoc": "^0.14.2",
|
||||||
"@radix-ui/react-accordion": "^1.1.2",
|
"@radix-ui/react-accordion": "^1.1.2",
|
||||||
"@radix-ui/react-navigation-menu": "^1.1.4",
|
"@radix-ui/react-navigation-menu": "^1.1.4",
|
||||||
|
"@tldraw/utils": "workspace:*",
|
||||||
"@types/broken-link-checker": "^0.7.1",
|
"@types/broken-link-checker": "^0.7.1",
|
||||||
"@types/node": "~20.11",
|
"@types/node": "~20.11",
|
||||||
"@types/sqlite3": "^3.1.9",
|
"@types/sqlite3": "^3.1.9",
|
||||||
|
|
|
@ -24,5 +24,10 @@
|
||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts",
|
||||||
"watcher.ts"
|
"watcher.ts"
|
||||||
],
|
],
|
||||||
"exclude": ["node_modules", ".next"]
|
"exclude": ["node_modules", ".next"],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../packages/utils"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
"roots": [
|
"roots": [
|
||||||
"<rootDir>"
|
"<rootDir>"
|
||||||
],
|
],
|
||||||
"testEnvironment": "jsdom",
|
"testEnvironment": "../../../packages/utils/patchedJestJsDom.js",
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
"node_modules/(?!(nanoid|nanoevents)/)"
|
"node_modules/(?!(nanoid|nanoevents)/)"
|
||||||
],
|
],
|
||||||
|
|
|
@ -15,7 +15,6 @@ import {
|
||||||
Vec,
|
Vec,
|
||||||
VecModel,
|
VecModel,
|
||||||
ZERO_INDEX_KEY,
|
ZERO_INDEX_KEY,
|
||||||
deepCopy,
|
|
||||||
getDefaultColorTheme,
|
getDefaultColorTheme,
|
||||||
resizeBox,
|
resizeBox,
|
||||||
structuredClone,
|
structuredClone,
|
||||||
|
@ -143,7 +142,7 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const next = deepCopy(shape)
|
const next = structuredClone(shape)
|
||||||
next.props.tail.x = newPoint.x / w
|
next.props.tail.x = newPoint.x / w
|
||||||
next.props.tail.y = newPoint.y / h
|
next.props.tail.y = newPoint.y / h
|
||||||
|
|
||||||
|
|
|
@ -84,7 +84,7 @@
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "config/jest/node",
|
"preset": "config/jest/node",
|
||||||
"testEnvironment": "jsdom",
|
"testEnvironment": "../../../packages/utils/patchedJestJsDom.js",
|
||||||
"fakeTimers": {
|
"fakeTimers": {
|
||||||
"enableGlobally": true
|
"enableGlobally": true
|
||||||
},
|
},
|
||||||
|
|
|
@ -47,7 +47,6 @@ import {
|
||||||
assert,
|
assert,
|
||||||
compact,
|
compact,
|
||||||
dedupe,
|
dedupe,
|
||||||
deepCopy,
|
|
||||||
getIndexAbove,
|
getIndexAbove,
|
||||||
getIndexBetween,
|
getIndexBetween,
|
||||||
getIndices,
|
getIndices,
|
||||||
|
@ -5224,7 +5223,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
? getIndexBetween(shape.index, siblingAbove.index)
|
? getIndexBetween(shape.index, siblingAbove.index)
|
||||||
: getIndexAbove(shape.index)
|
: getIndexAbove(shape.index)
|
||||||
|
|
||||||
let newShape: TLShape = deepCopy(shape)
|
let newShape: TLShape = structuredClone(shape)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.isShapeOfType<TLArrowShape>(shape, 'arrow') &&
|
this.isShapeOfType<TLArrowShape>(shape, 'arrow') &&
|
||||||
|
@ -7867,13 +7866,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
let newShape: TLShape
|
let newShape: TLShape
|
||||||
|
|
||||||
if (preserveIds) {
|
if (preserveIds) {
|
||||||
newShape = deepCopy(shape)
|
newShape = structuredClone(shape)
|
||||||
idMap.set(shape.id, shape.id)
|
idMap.set(shape.id, shape.id)
|
||||||
} else {
|
} else {
|
||||||
const id = idMap.get(shape.id)!
|
const id = idMap.get(shape.id)!
|
||||||
|
|
||||||
// Create the new shape (new except for the id)
|
// Create the new shape (new except for the id)
|
||||||
newShape = deepCopy({ ...shape, id })
|
newShape = structuredClone({ ...shape, id })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rootShapeIds.includes(shape.id)) {
|
if (rootShapeIds.includes(shape.id)) {
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "config/jest/node",
|
"preset": "config/jest/node",
|
||||||
"testEnvironment": "jsdom",
|
"testEnvironment": "../../../packages/utils/patchedJestJsDom.js",
|
||||||
"fakeTimers": {
|
"fakeTimers": {
|
||||||
"enableGlobally": true
|
"enableGlobally": true
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { STRUCTURED_CLONE_OBJECT_PROTOTYPE } from '@tldraw/utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Freeze an object when in development mode. Copied from
|
* Freeze an object when in development mode. Copied from
|
||||||
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze
|
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze
|
||||||
|
@ -17,7 +19,15 @@ export function devFreeze<T>(object: T): T {
|
||||||
return object
|
return object
|
||||||
}
|
}
|
||||||
const proto = Object.getPrototypeOf(object)
|
const proto = Object.getPrototypeOf(object)
|
||||||
if (proto && !(proto === Array.prototype || proto === Object.prototype)) {
|
if (
|
||||||
|
proto &&
|
||||||
|
!(
|
||||||
|
Array.isArray(object) ||
|
||||||
|
proto === Object.prototype ||
|
||||||
|
proto === null ||
|
||||||
|
proto === STRUCTURED_CLONE_OBJECT_PROTOTYPE
|
||||||
|
)
|
||||||
|
) {
|
||||||
console.error('cannot include non-js data in a record', object)
|
console.error('cannot include non-js data in a record', object)
|
||||||
throw new Error('cannot include non-js data in a record')
|
throw new Error('cannot include non-js data in a record')
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,7 @@
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "config/jest/node",
|
"preset": "config/jest/node",
|
||||||
"testEnvironment": "jsdom",
|
"testEnvironment": "../../../packages/utils/patchedJestJsDom.js",
|
||||||
"fakeTimers": {
|
"fakeTimers": {
|
||||||
"enableGlobally": true
|
"enableGlobally": true
|
||||||
},
|
},
|
||||||
|
|
|
@ -27,11 +27,11 @@ import {
|
||||||
Vec,
|
Vec,
|
||||||
arrowShapeMigrations,
|
arrowShapeMigrations,
|
||||||
arrowShapeProps,
|
arrowShapeProps,
|
||||||
deepCopy,
|
|
||||||
getArrowTerminalsInArrowSpace,
|
getArrowTerminalsInArrowSpace,
|
||||||
getDefaultColorTheme,
|
getDefaultColorTheme,
|
||||||
mapObjectMapValues,
|
mapObjectMapValues,
|
||||||
objectMapEntries,
|
objectMapEntries,
|
||||||
|
structuredClone,
|
||||||
toDomPrecision,
|
toDomPrecision,
|
||||||
useIsEditing,
|
useIsEditing,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
|
@ -194,7 +194,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
|
|
||||||
// Start or end, pointing the arrow...
|
// Start or end, pointing the arrow...
|
||||||
|
|
||||||
const next = deepCopy(shape) as TLArrowShape
|
const next = structuredClone(shape) as TLArrowShape
|
||||||
|
|
||||||
if (this.editor.inputs.ctrlKey) {
|
if (this.editor.inputs.ctrlKey) {
|
||||||
// todo: maybe double check that this isn't equal to the other handle too?
|
// todo: maybe double check that this isn't equal to the other handle too?
|
||||||
|
@ -420,7 +420,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
|
|
||||||
const terminals = getArrowTerminalsInArrowSpace(this.editor, shape)
|
const terminals = getArrowTerminalsInArrowSpace(this.editor, shape)
|
||||||
|
|
||||||
const { start, end } = deepCopy<TLArrowShape['props']>(shape.props)
|
const { start, end } = structuredClone<TLArrowShape['props']>(shape.props)
|
||||||
let { bend } = shape.props
|
let { bend } = shape.props
|
||||||
|
|
||||||
// Rescale start handle if it's not bound to a shape
|
// Rescale start handle if it's not bound to a shape
|
||||||
|
|
|
@ -7,9 +7,9 @@ import {
|
||||||
TLOnDoubleClickHandler,
|
TLOnDoubleClickHandler,
|
||||||
TLShapePartial,
|
TLShapePartial,
|
||||||
Vec,
|
Vec,
|
||||||
deepCopy,
|
|
||||||
imageShapeMigrations,
|
imageShapeMigrations,
|
||||||
imageShapeProps,
|
imageShapeProps,
|
||||||
|
structuredClone,
|
||||||
toDomPrecision,
|
toDomPrecision,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
@ -254,7 +254,7 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const crop = deepCopy(props.crop) || {
|
const crop = structuredClone(props.crop) || {
|
||||||
topLeft: { x: 0, y: 0 },
|
topLeft: { x: 0, y: 0 },
|
||||||
bottomRight: { x: 1, y: 1 },
|
bottomRight: { x: 1, y: 1 },
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,8 @@ import {
|
||||||
TLGeoShape,
|
TLGeoShape,
|
||||||
TLLineShape,
|
TLLineShape,
|
||||||
createShapeId,
|
createShapeId,
|
||||||
deepCopy,
|
|
||||||
sortByIndex,
|
sortByIndex,
|
||||||
|
structuredClone,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import { TestEditor } from '../../../test/TestEditor'
|
import { TestEditor } from '../../../test/TestEditor'
|
||||||
import { TL } from '../../../test/test-jsx'
|
import { TL } from '../../../test/test-jsx'
|
||||||
|
@ -278,7 +278,7 @@ describe('Misc', () => {
|
||||||
it('preserves handle positions on spline type change', () => {
|
it('preserves handle positions on spline type change', () => {
|
||||||
editor.select(id)
|
editor.select(id)
|
||||||
const shape = getShape()
|
const shape = getShape()
|
||||||
const prevPoints = deepCopy(shape.props.points)
|
const prevPoints = structuredClone(shape.props.points)
|
||||||
|
|
||||||
editor.updateShapes([
|
editor.updateShapes([
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {
|
||||||
TLImageShapeCrop,
|
TLImageShapeCrop,
|
||||||
TLShapePartial,
|
TLShapePartial,
|
||||||
Vec,
|
Vec,
|
||||||
deepCopy,
|
structuredClone,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
|
|
||||||
export type ShapeWithCrop = TLBaseShape<string, { w: number; h: number; crop: TLImageShapeCrop }>
|
export type ShapeWithCrop = TLBaseShape<string, { w: number; h: number; crop: TLImageShapeCrop }>
|
||||||
|
@ -44,7 +44,7 @@ export function getTranslateCroppedImageChange(
|
||||||
|
|
||||||
const yCrop = oldCrop.bottomRight.y - oldCrop.topLeft.y
|
const yCrop = oldCrop.bottomRight.y - oldCrop.topLeft.y
|
||||||
const xCrop = oldCrop.bottomRight.x - oldCrop.topLeft.x
|
const xCrop = oldCrop.bottomRight.x - oldCrop.topLeft.x
|
||||||
const newCrop = deepCopy(oldCrop)
|
const newCrop = structuredClone(oldCrop)
|
||||||
|
|
||||||
newCrop.topLeft.x = Math.min(1 - xCrop, Math.max(0, newCrop.topLeft.x - delta.x / w))
|
newCrop.topLeft.x = Math.min(1 - xCrop, Math.max(0, newCrop.topLeft.x - delta.x / w))
|
||||||
newCrop.topLeft.y = Math.min(1 - yCrop, Math.max(0, newCrop.topLeft.y - delta.y / h))
|
newCrop.topLeft.y = Math.min(1 - yCrop, Math.max(0, newCrop.topLeft.y - delta.y / h))
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
TLPointerEventInfo,
|
TLPointerEventInfo,
|
||||||
TLShapePartial,
|
TLShapePartial,
|
||||||
Vec,
|
Vec,
|
||||||
deepCopy,
|
structuredClone,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import { MIN_CROP_SIZE } from './Crop/crop-constants'
|
import { MIN_CROP_SIZE } from './Crop/crop-constants'
|
||||||
import { CursorTypeMap } from './PointingResizeHandle'
|
import { CursorTypeMap } from './PointingResizeHandle'
|
||||||
|
@ -101,7 +101,7 @@ export class Cropping extends StateNode {
|
||||||
const change = currentPagePoint.clone().sub(originPagePoint).rot(-shape.rotation)
|
const change = currentPagePoint.clone().sub(originPagePoint).rot(-shape.rotation)
|
||||||
|
|
||||||
const crop = props.crop ?? this.getDefaultCrop()
|
const crop = props.crop ?? this.getDefaultCrop()
|
||||||
const newCrop = deepCopy(crop)
|
const newCrop = structuredClone(crop)
|
||||||
|
|
||||||
const newPoint = new Vec(shape.x, shape.y)
|
const newPoint = new Vec(shape.x, shape.y)
|
||||||
const pointDelta = new Vec(0, 0)
|
const pointDelta = new Vec(0, 0)
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
import { defineMigrations } from '@tldraw/store'
|
import { defineMigrations } from '@tldraw/store'
|
||||||
import { IndexKey, deepCopy, getIndices, objectMapFromEntries, sortByIndex } from '@tldraw/utils'
|
import {
|
||||||
|
IndexKey,
|
||||||
|
getIndices,
|
||||||
|
objectMapFromEntries,
|
||||||
|
sortByIndex,
|
||||||
|
structuredClone,
|
||||||
|
} from '@tldraw/utils'
|
||||||
import { T } from '@tldraw/validate'
|
import { T } from '@tldraw/validate'
|
||||||
import { StyleProp } from '../styles/StyleProp'
|
import { StyleProp } from '../styles/StyleProp'
|
||||||
import { DefaultColorStyle } from '../styles/TLColorStyle'
|
import { DefaultColorStyle } from '../styles/TLColorStyle'
|
||||||
|
@ -52,14 +58,14 @@ export const lineShapeMigrations = defineMigrations({
|
||||||
migrators: {
|
migrators: {
|
||||||
[lineShapeVersions.AddSnapHandles]: {
|
[lineShapeVersions.AddSnapHandles]: {
|
||||||
up: (record: any) => {
|
up: (record: any) => {
|
||||||
const handles = deepCopy(record.props.handles as Record<string, any>)
|
const handles = structuredClone(record.props.handles as Record<string, any>)
|
||||||
for (const id in handles) {
|
for (const id in handles) {
|
||||||
handles[id].canSnap = true
|
handles[id].canSnap = true
|
||||||
}
|
}
|
||||||
return { ...record, props: { ...record.props, handles } }
|
return { ...record, props: { ...record.props, handles } }
|
||||||
},
|
},
|
||||||
down: (record: any) => {
|
down: (record: any) => {
|
||||||
const handles = deepCopy(record.props.handles as Record<string, any>)
|
const handles = structuredClone(record.props.handles as Record<string, any>)
|
||||||
for (const id in handles) {
|
for (const id in handles) {
|
||||||
delete handles[id].canSnap
|
delete handles[id].canSnap
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "config/jest/node",
|
"preset": "config/jest/node",
|
||||||
"testEnvironment": "jsdom",
|
"testEnvironment": "../../../packages/utils/patchedJestJsDom.js",
|
||||||
"moduleNameMapper": {
|
"moduleNameMapper": {
|
||||||
"^~(.*)": "<rootDir>/src/$1"
|
"^~(.*)": "<rootDir>/src/$1"
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,10 +13,12 @@ import { DocumentRecordType, PageRecordType, TLDOCUMENT_ID } from '@tldraw/tlsch
|
||||||
import {
|
import {
|
||||||
IndexKey,
|
IndexKey,
|
||||||
Result,
|
Result,
|
||||||
|
assert,
|
||||||
assertExists,
|
assertExists,
|
||||||
exhaustiveSwitchError,
|
exhaustiveSwitchError,
|
||||||
getOwnProperty,
|
getOwnProperty,
|
||||||
hasOwnProperty,
|
hasOwnProperty,
|
||||||
|
isNativeStructuredClone,
|
||||||
objectMapEntries,
|
objectMapEntries,
|
||||||
objectMapKeys,
|
objectMapKeys,
|
||||||
} from '@tldraw/utils'
|
} from '@tldraw/utils'
|
||||||
|
@ -208,6 +210,12 @@ export class TLSyncRoom<R extends UnknownRecord> {
|
||||||
public readonly schema: StoreSchema<R, any>,
|
public readonly schema: StoreSchema<R, any>,
|
||||||
snapshot?: RoomSnapshot
|
snapshot?: RoomSnapshot
|
||||||
) {
|
) {
|
||||||
|
assert(
|
||||||
|
isNativeStructuredClone,
|
||||||
|
'TLSyncRoom is supposed to run either on Cloudflare Workers' +
|
||||||
|
'or on a 18+ version of Node.js, which both support the native structuredClone API'
|
||||||
|
)
|
||||||
|
|
||||||
// do a json serialization cycle to make sure the schema has no 'undefined' values
|
// do a json serialization cycle to make sure the schema has no 'undefined' values
|
||||||
this.serializedSchema = JSON.parse(JSON.stringify(schema.serialize()))
|
this.serializedSchema = JSON.parse(JSON.stringify(schema.serialize()))
|
||||||
|
|
||||||
|
|
|
@ -37,9 +37,6 @@ export function debounce<T extends unknown[], U>(callback: (...args: T) => Promi
|
||||||
// @public
|
// @public
|
||||||
export function dedupe<T>(input: T[], equals?: (a: any, b: any) => boolean): T[];
|
export function dedupe<T>(input: T[], equals?: (a: any, b: any) => boolean): T[];
|
||||||
|
|
||||||
// @public
|
|
||||||
export function deepCopy<T = unknown>(obj: T): T;
|
|
||||||
|
|
||||||
// @internal
|
// @internal
|
||||||
export function deleteFromLocalStorage(key: string): void;
|
export function deleteFromLocalStorage(key: string): void;
|
||||||
|
|
||||||
|
@ -140,6 +137,9 @@ export function invLerp(a: number, b: number, t: number): number;
|
||||||
// @public
|
// @public
|
||||||
export function isDefined<T>(value: T): value is typeof value extends undefined ? never : T;
|
export function isDefined<T>(value: T): value is typeof value extends undefined ? never : T;
|
||||||
|
|
||||||
|
// @internal (undocumented)
|
||||||
|
export const isNativeStructuredClone: boolean;
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export function isNonNull<T>(value: T): value is typeof value extends null ? never : T;
|
export function isNonNull<T>(value: T): value is typeof value extends null ? never : T;
|
||||||
|
|
||||||
|
@ -307,6 +307,9 @@ export function sortByIndex<T extends {
|
||||||
index: IndexKey;
|
index: IndexKey;
|
||||||
}>(a: T, b: T): -1 | 0 | 1;
|
}>(a: T, b: T): -1 | 0 | 1;
|
||||||
|
|
||||||
|
// @internal
|
||||||
|
export const STRUCTURED_CLONE_OBJECT_PROTOTYPE: any;
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
const structuredClone_2: <T>(i: T) => T;
|
const structuredClone_2: <T>(i: T) => T;
|
||||||
export { structuredClone_2 as structuredClone }
|
export { structuredClone_2 as structuredClone }
|
||||||
|
|
|
@ -357,72 +357,6 @@
|
||||||
],
|
],
|
||||||
"name": "dedupe"
|
"name": "dedupe"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"kind": "Function",
|
|
||||||
"canonicalReference": "@tldraw/utils!deepCopy:function(1)",
|
|
||||||
"docComment": "/**\n * Deep copy function for TypeScript.\n *\n * @param obj - Target value to be copied.\n *\n * @example\n * ```ts\n * const A = deepCopy({ a: 1, b: { c: 2 } })\n * ```\n *\n * @see\n *\n * Source - project, ts-deeply https://github.com/ykdr2017/ts-deepcopy\n *\n * @see\n *\n * Code - pen https://codepen.io/erikvullings/pen/ejyBYg\n *\n * @public\n */\n",
|
|
||||||
"excerptTokens": [
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "export declare function deepCopy<T = "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "unknown"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": ">(obj: "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "T"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "): "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "T"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": ";"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"fileUrlPath": "packages/utils/src/lib/object.ts",
|
|
||||||
"returnTypeTokenRange": {
|
|
||||||
"startIndex": 5,
|
|
||||||
"endIndex": 6
|
|
||||||
},
|
|
||||||
"releaseTag": "Public",
|
|
||||||
"overloadIndex": 1,
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"parameterName": "obj",
|
|
||||||
"parameterTypeTokenRange": {
|
|
||||||
"startIndex": 3,
|
|
||||||
"endIndex": 4
|
|
||||||
},
|
|
||||||
"isOptional": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"typeParameters": [
|
|
||||||
{
|
|
||||||
"typeParameterName": "T",
|
|
||||||
"constraintTokenRange": {
|
|
||||||
"startIndex": 0,
|
|
||||||
"endIndex": 0
|
|
||||||
},
|
|
||||||
"defaultTypeTokenRange": {
|
|
||||||
"startIndex": 1,
|
|
||||||
"endIndex": 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"name": "deepCopy"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"kind": "TypeAlias",
|
"kind": "TypeAlias",
|
||||||
"canonicalReference": "@tldraw/utils!ErrorResult:type",
|
"canonicalReference": "@tldraw/utils!ErrorResult:type",
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"jest-environment-jsdom": "^29.4.3",
|
||||||
"lazyrepo": "0.0.0-alpha.27"
|
"lazyrepo": "0.0.0-alpha.27"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import JSDOMEnvironment from 'jest-environment-jsdom'
|
||||||
|
|
||||||
|
export default class FixJSDOMEnvironment extends JSDOMEnvironment {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args)
|
||||||
|
|
||||||
|
// fixes https://github.com/jsdom/jsdom/issues/3363
|
||||||
|
this.global.structuredClone = structuredClone
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,7 +27,6 @@ export { MediaHelpers } from './lib/media'
|
||||||
export { invLerp, lerp, modulate, rng } from './lib/number'
|
export { invLerp, lerp, modulate, rng } from './lib/number'
|
||||||
export {
|
export {
|
||||||
areObjectsShallowEqual,
|
areObjectsShallowEqual,
|
||||||
deepCopy,
|
|
||||||
filterEntries,
|
filterEntries,
|
||||||
getOwnProperty,
|
getOwnProperty,
|
||||||
hasOwnProperty,
|
hasOwnProperty,
|
||||||
|
@ -64,5 +63,12 @@ export {
|
||||||
} from './lib/storage'
|
} from './lib/storage'
|
||||||
export { fpsThrottle, throttleToNextFrame } from './lib/throttle'
|
export { fpsThrottle, throttleToNextFrame } from './lib/throttle'
|
||||||
export type { Expand, RecursivePartial, Required } from './lib/types'
|
export type { Expand, RecursivePartial, Required } from './lib/types'
|
||||||
export { isDefined, isNonNull, isNonNullish, structuredClone } from './lib/value'
|
export {
|
||||||
|
STRUCTURED_CLONE_OBJECT_PROTOTYPE,
|
||||||
|
isDefined,
|
||||||
|
isNativeStructuredClone,
|
||||||
|
isNonNull,
|
||||||
|
isNonNullish,
|
||||||
|
structuredClone,
|
||||||
|
} from './lib/value'
|
||||||
export { warnDeprecatedGetter } from './lib/warnDeprecatedGetter'
|
export { warnDeprecatedGetter } from './lib/warnDeprecatedGetter'
|
||||||
|
|
|
@ -19,40 +19,6 @@ export function getOwnProperty(obj: object, key: string): unknown {
|
||||||
return obj[key]
|
return obj[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Deep copy function for TypeScript.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
*
|
|
||||||
* ```ts
|
|
||||||
* const A = deepCopy({ a: 1, b: { c: 2 } })
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @param obj - Target value to be copied.
|
|
||||||
* @public
|
|
||||||
* @see Source - project, ts-deeply https://github.com/ykdr2017/ts-deepcopy
|
|
||||||
* @see Code - pen https://codepen.io/erikvullings/pen/ejyBYg
|
|
||||||
*/
|
|
||||||
export function deepCopy<T = unknown>(obj: T): T {
|
|
||||||
if (!obj) return obj
|
|
||||||
if (Array.isArray(obj)) {
|
|
||||||
const arr: unknown[] = []
|
|
||||||
const length = obj.length
|
|
||||||
for (let i = 0; i < length; i++) arr.push(deepCopy(obj[i]))
|
|
||||||
return arr as unknown as T
|
|
||||||
} else if (typeof obj === 'object') {
|
|
||||||
const keys = Object.keys(obj!)
|
|
||||||
const length = keys.length
|
|
||||||
const newObject: any = {}
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
const key = keys[i]
|
|
||||||
newObject[key] = deepCopy((obj as any)[key])
|
|
||||||
}
|
|
||||||
return newObject
|
|
||||||
}
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An alias for `Object.keys` that treats the object as a map and so preserves the type of the keys.
|
* An alias for `Object.keys` that treats the object as a map and so preserves the type of the keys.
|
||||||
*
|
*
|
||||||
|
|
|
@ -30,12 +30,44 @@ export function isNonNullish<T>(
|
||||||
return value !== null && value !== undefined
|
return value !== null && value !== undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getStructuredClone(): [<T>(i: T) => T, boolean] {
|
||||||
|
if (typeof globalThis !== 'undefined' && (globalThis as any).structuredClone) {
|
||||||
|
return [globalThis.structuredClone as <T>(i: T) => T, true]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof global !== 'undefined' && (global as any).structuredClone) {
|
||||||
|
return [global.structuredClone as <T>(i: T) => T, true]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined' && (window as any).structuredClone) {
|
||||||
|
return [window.structuredClone as <T>(i: T) => T, true]
|
||||||
|
}
|
||||||
|
|
||||||
|
return [<T>(i: T): T => (i ? JSON.parse(JSON.stringify(i)) : i), false]
|
||||||
|
}
|
||||||
|
|
||||||
|
const _structuredClone = getStructuredClone()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a deep copy of a value. Uses the structuredClone API if available, otherwise uses JSON.parse(JSON.stringify()).
|
* Create a deep copy of a value. Uses the structuredClone API if available, otherwise uses JSON.parse(JSON.stringify()).
|
||||||
*
|
*
|
||||||
* @param i - The value to clone.
|
* @param i - The value to clone.
|
||||||
* @public */
|
* @public */
|
||||||
export const structuredClone =
|
export const structuredClone = _structuredClone[0]
|
||||||
typeof window !== 'undefined' && (window as any).structuredClone
|
|
||||||
? (window.structuredClone as <T>(i: T) => T)
|
/**
|
||||||
: <T>(i: T): T => (i ? JSON.parse(JSON.stringify(i)) : i)
|
* @internal
|
||||||
|
*/
|
||||||
|
export const isNativeStructuredClone = _structuredClone[1]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When we patch structuredClone in jsdom for testing (see https://github.com/jsdom/jsdom/issues/3363),
|
||||||
|
* the Object that is used as a prototype for the cloned object is not the same as the Object in
|
||||||
|
* the code under test (that comes from jsdom's fake global context). This constant is used in
|
||||||
|
* our code to work around this case.
|
||||||
|
*
|
||||||
|
* This is also the case for Array prototype, but that problem can be worked around with an
|
||||||
|
* Array.isArray() check.
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export const STRUCTURED_CLONE_OBJECT_PROTOTYPE = Object.getPrototypeOf(structuredClone({}))
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {
|
import {
|
||||||
IndexKey,
|
IndexKey,
|
||||||
JsonValue,
|
JsonValue,
|
||||||
|
STRUCTURED_CLONE_OBJECT_PROTOTYPE,
|
||||||
exhaustiveSwitchError,
|
exhaustiveSwitchError,
|
||||||
getOwnProperty,
|
getOwnProperty,
|
||||||
hasOwnProperty,
|
hasOwnProperty,
|
||||||
|
@ -698,7 +699,9 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||||
return (
|
return (
|
||||||
typeof value === 'object' &&
|
typeof value === 'object' &&
|
||||||
value !== null &&
|
value !== null &&
|
||||||
(value.constructor === Object || !value.constructor)
|
(Object.getPrototypeOf(value) === Object.prototype ||
|
||||||
|
Object.getPrototypeOf(value) === null ||
|
||||||
|
Object.getPrototypeOf(value) === STRUCTURED_CLONE_OBJECT_PROTOTYPE)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ export async function preparePackage({ sourcePackageDir }: { sourcePackageDir: s
|
||||||
const cssFiles = glob.sync(path.join(sourcePackageDir, '*.css'))
|
const cssFiles = glob.sync(path.join(sourcePackageDir, '*.css'))
|
||||||
|
|
||||||
// construct the final package.json
|
// construct the final package.json
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
const newManifest = structuredClone({
|
const newManifest = structuredClone({
|
||||||
// filter out comments
|
// filter out comments
|
||||||
...Object.fromEntries(
|
...Object.fromEntries(
|
||||||
|
|
|
@ -7188,6 +7188,7 @@ __metadata:
|
||||||
"@microsoft/tsdoc": "npm:^0.14.2"
|
"@microsoft/tsdoc": "npm:^0.14.2"
|
||||||
"@radix-ui/react-accordion": "npm:^1.1.2"
|
"@radix-ui/react-accordion": "npm:^1.1.2"
|
||||||
"@radix-ui/react-navigation-menu": "npm:^1.1.4"
|
"@radix-ui/react-navigation-menu": "npm:^1.1.4"
|
||||||
|
"@tldraw/utils": "workspace:*"
|
||||||
"@types/broken-link-checker": "npm:^0.7.1"
|
"@types/broken-link-checker": "npm:^0.7.1"
|
||||||
"@types/node": "npm:~20.11"
|
"@types/node": "npm:~20.11"
|
||||||
"@types/sqlite3": "npm:^3.1.9"
|
"@types/sqlite3": "npm:^3.1.9"
|
||||||
|
@ -7463,6 +7464,7 @@ __metadata:
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@tldraw/utils@workspace:packages/utils"
|
resolution: "@tldraw/utils@workspace:packages/utils"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
jest-environment-jsdom: "npm:^29.4.3"
|
||||||
lazyrepo: "npm:0.0.0-alpha.27"
|
lazyrepo: "npm:0.0.0-alpha.27"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
Ładowanie…
Reference in New Issue