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 tests
This commit is contained in:
parent
1951fc0e47
commit
d7b80baa31
32 changed files with 135 additions and 141 deletions
|
@ -28,4 +28,6 @@ apps/vscode/extension/editor/tldraw-assets.json
|
|||
**/scripts/upload-sourcemaps.js
|
||||
**/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',
|
||||
},
|
||||
],
|
||||
'no-restricted-globals': [
|
||||
'error',
|
||||
{ name: 'structuredClone', message: 'Use structuredClone from @tldraw/util instead' },
|
||||
],
|
||||
},
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
|
|
6
.github/workflows/checks.yml
vendored
6
.github/workflows/checks.yml
vendored
|
@ -38,15 +38,15 @@ jobs:
|
|||
- name: Check for installation warnings
|
||||
run: 'yarn | grep -vzq "with warnings"'
|
||||
|
||||
- name: Check tsconfigs
|
||||
run: yarn check-tsconfigs
|
||||
|
||||
- name: Typecheck
|
||||
run: yarn build-types
|
||||
|
||||
- name: Check scripts
|
||||
run: yarn check-scripts
|
||||
|
||||
- name: Check tsconfigs
|
||||
run: yarn check-tsconfigs
|
||||
|
||||
- name: Check PR template
|
||||
run: yarn update-pr-template --check
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { SearchResult } from '@/types/search-types'
|
||||
import { getDb } from '@/utils/ContentDatabase'
|
||||
import { SEARCH_RESULTS, searchBucket, sectionTypeBucket } from '@/utils/search-api'
|
||||
import { structuredClone } from '@tldraw/utils'
|
||||
import assert from 'assert'
|
||||
import { NextRequest } from 'next/server'
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { SearchResult } from '@/types/search-types'
|
||||
import { getDb } from '@/utils/ContentDatabase'
|
||||
import { SEARCH_RESULTS, searchBucket, sectionTypeBucket } from '@/utils/search-api'
|
||||
import { structuredClone } from '@tldraw/utils'
|
||||
import { NextRequest } from 'next/server'
|
||||
|
||||
type Data = {
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
"@microsoft/tsdoc": "^0.14.2",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-navigation-menu": "^1.1.4",
|
||||
"@tldraw/utils": "workspace:*",
|
||||
"@types/broken-link-checker": "^0.7.1",
|
||||
"@types/node": "~20.11",
|
||||
"@types/sqlite3": "^3.1.9",
|
||||
|
|
|
@ -24,5 +24,10 @@
|
|||
".next/types/**/*.ts",
|
||||
"watcher.ts"
|
||||
],
|
||||
"exclude": ["node_modules", ".next"]
|
||||
"exclude": ["node_modules", ".next"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../packages/utils"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
"roots": [
|
||||
"<rootDir>"
|
||||
],
|
||||
"testEnvironment": "jsdom",
|
||||
"testEnvironment": "../../../packages/utils/patchedJestJsDom.js",
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!(nanoid|nanoevents)/)"
|
||||
],
|
||||
|
|
|
@ -15,7 +15,6 @@ import {
|
|||
Vec,
|
||||
VecModel,
|
||||
ZERO_INDEX_KEY,
|
||||
deepCopy,
|
||||
getDefaultColorTheme,
|
||||
resizeBox,
|
||||
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.y = newPoint.y / h
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@
|
|||
},
|
||||
"jest": {
|
||||
"preset": "config/jest/node",
|
||||
"testEnvironment": "jsdom",
|
||||
"testEnvironment": "../../../packages/utils/patchedJestJsDom.js",
|
||||
"fakeTimers": {
|
||||
"enableGlobally": true
|
||||
},
|
||||
|
|
|
@ -47,7 +47,6 @@ import {
|
|||
assert,
|
||||
compact,
|
||||
dedupe,
|
||||
deepCopy,
|
||||
getIndexAbove,
|
||||
getIndexBetween,
|
||||
getIndices,
|
||||
|
@ -5224,7 +5223,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
? getIndexBetween(shape.index, siblingAbove.index)
|
||||
: getIndexAbove(shape.index)
|
||||
|
||||
let newShape: TLShape = deepCopy(shape)
|
||||
let newShape: TLShape = structuredClone(shape)
|
||||
|
||||
if (
|
||||
this.isShapeOfType<TLArrowShape>(shape, 'arrow') &&
|
||||
|
@ -7867,13 +7866,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
let newShape: TLShape
|
||||
|
||||
if (preserveIds) {
|
||||
newShape = deepCopy(shape)
|
||||
newShape = structuredClone(shape)
|
||||
idMap.set(shape.id, shape.id)
|
||||
} else {
|
||||
const id = idMap.get(shape.id)!
|
||||
|
||||
// Create the new shape (new except for the id)
|
||||
newShape = deepCopy({ ...shape, id })
|
||||
newShape = structuredClone({ ...shape, id })
|
||||
}
|
||||
|
||||
if (rootShapeIds.includes(shape.id)) {
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
},
|
||||
"jest": {
|
||||
"preset": "config/jest/node",
|
||||
"testEnvironment": "jsdom",
|
||||
"testEnvironment": "../../../packages/utils/patchedJestJsDom.js",
|
||||
"fakeTimers": {
|
||||
"enableGlobally": true
|
||||
},
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { STRUCTURED_CLONE_OBJECT_PROTOTYPE } from '@tldraw/utils'
|
||||
|
||||
/**
|
||||
* Freeze an object when in development mode. Copied from
|
||||
* 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
|
||||
}
|
||||
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)
|
||||
throw new Error('cannot include non-js data in a record')
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@
|
|||
},
|
||||
"jest": {
|
||||
"preset": "config/jest/node",
|
||||
"testEnvironment": "jsdom",
|
||||
"testEnvironment": "../../../packages/utils/patchedJestJsDom.js",
|
||||
"fakeTimers": {
|
||||
"enableGlobally": true
|
||||
},
|
||||
|
|
|
@ -27,11 +27,11 @@ import {
|
|||
Vec,
|
||||
arrowShapeMigrations,
|
||||
arrowShapeProps,
|
||||
deepCopy,
|
||||
getArrowTerminalsInArrowSpace,
|
||||
getDefaultColorTheme,
|
||||
mapObjectMapValues,
|
||||
objectMapEntries,
|
||||
structuredClone,
|
||||
toDomPrecision,
|
||||
useIsEditing,
|
||||
} from '@tldraw/editor'
|
||||
|
@ -194,7 +194,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
|
||||
// Start or end, pointing the arrow...
|
||||
|
||||
const next = deepCopy(shape) as TLArrowShape
|
||||
const next = structuredClone(shape) as TLArrowShape
|
||||
|
||||
if (this.editor.inputs.ctrlKey) {
|
||||
// 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 { start, end } = deepCopy<TLArrowShape['props']>(shape.props)
|
||||
const { start, end } = structuredClone<TLArrowShape['props']>(shape.props)
|
||||
let { bend } = shape.props
|
||||
|
||||
// Rescale start handle if it's not bound to a shape
|
||||
|
|
|
@ -7,9 +7,9 @@ import {
|
|||
TLOnDoubleClickHandler,
|
||||
TLShapePartial,
|
||||
Vec,
|
||||
deepCopy,
|
||||
imageShapeMigrations,
|
||||
imageShapeProps,
|
||||
structuredClone,
|
||||
toDomPrecision,
|
||||
} from '@tldraw/editor'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
@ -254,7 +254,7 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
|||
return
|
||||
}
|
||||
|
||||
const crop = deepCopy(props.crop) || {
|
||||
const crop = structuredClone(props.crop) || {
|
||||
topLeft: { x: 0, y: 0 },
|
||||
bottomRight: { x: 1, y: 1 },
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@ import {
|
|||
TLGeoShape,
|
||||
TLLineShape,
|
||||
createShapeId,
|
||||
deepCopy,
|
||||
sortByIndex,
|
||||
structuredClone,
|
||||
} from '@tldraw/editor'
|
||||
import { TestEditor } from '../../../test/TestEditor'
|
||||
import { TL } from '../../../test/test-jsx'
|
||||
|
@ -278,7 +278,7 @@ describe('Misc', () => {
|
|||
it('preserves handle positions on spline type change', () => {
|
||||
editor.select(id)
|
||||
const shape = getShape()
|
||||
const prevPoints = deepCopy(shape.props.points)
|
||||
const prevPoints = structuredClone(shape.props.points)
|
||||
|
||||
editor.updateShapes([
|
||||
{
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
TLImageShapeCrop,
|
||||
TLShapePartial,
|
||||
Vec,
|
||||
deepCopy,
|
||||
structuredClone,
|
||||
} from '@tldraw/editor'
|
||||
|
||||
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 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.y = Math.min(1 - yCrop, Math.max(0, newCrop.topLeft.y - delta.y / h))
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
TLPointerEventInfo,
|
||||
TLShapePartial,
|
||||
Vec,
|
||||
deepCopy,
|
||||
structuredClone,
|
||||
} from '@tldraw/editor'
|
||||
import { MIN_CROP_SIZE } from './Crop/crop-constants'
|
||||
import { CursorTypeMap } from './PointingResizeHandle'
|
||||
|
@ -101,7 +101,7 @@ export class Cropping extends StateNode {
|
|||
const change = currentPagePoint.clone().sub(originPagePoint).rot(-shape.rotation)
|
||||
|
||||
const crop = props.crop ?? this.getDefaultCrop()
|
||||
const newCrop = deepCopy(crop)
|
||||
const newCrop = structuredClone(crop)
|
||||
|
||||
const newPoint = new Vec(shape.x, shape.y)
|
||||
const pointDelta = new Vec(0, 0)
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
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 { StyleProp } from '../styles/StyleProp'
|
||||
import { DefaultColorStyle } from '../styles/TLColorStyle'
|
||||
|
@ -52,14 +58,14 @@ export const lineShapeMigrations = defineMigrations({
|
|||
migrators: {
|
||||
[lineShapeVersions.AddSnapHandles]: {
|
||||
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) {
|
||||
handles[id].canSnap = true
|
||||
}
|
||||
return { ...record, props: { ...record.props, handles } }
|
||||
},
|
||||
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) {
|
||||
delete handles[id].canSnap
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"jest": {
|
||||
"preset": "config/jest/node",
|
||||
"testEnvironment": "jsdom",
|
||||
"testEnvironment": "../../../packages/utils/patchedJestJsDom.js",
|
||||
"moduleNameMapper": {
|
||||
"^~(.*)": "<rootDir>/src/$1"
|
||||
},
|
||||
|
|
|
@ -13,10 +13,12 @@ import { DocumentRecordType, PageRecordType, TLDOCUMENT_ID } from '@tldraw/tlsch
|
|||
import {
|
||||
IndexKey,
|
||||
Result,
|
||||
assert,
|
||||
assertExists,
|
||||
exhaustiveSwitchError,
|
||||
getOwnProperty,
|
||||
hasOwnProperty,
|
||||
isNativeStructuredClone,
|
||||
objectMapEntries,
|
||||
objectMapKeys,
|
||||
} from '@tldraw/utils'
|
||||
|
@ -208,6 +210,12 @@ export class TLSyncRoom<R extends UnknownRecord> {
|
|||
public readonly schema: StoreSchema<R, any>,
|
||||
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
|
||||
this.serializedSchema = JSON.parse(JSON.stringify(schema.serialize()))
|
||||
|
||||
|
|
|
@ -37,9 +37,6 @@ export function debounce<T extends unknown[], U>(callback: (...args: T) => Promi
|
|||
// @public
|
||||
export function dedupe<T>(input: T[], equals?: (a: any, b: any) => boolean): T[];
|
||||
|
||||
// @public
|
||||
export function deepCopy<T = unknown>(obj: T): T;
|
||||
|
||||
// @internal
|
||||
export function deleteFromLocalStorage(key: string): void;
|
||||
|
||||
|
@ -140,6 +137,9 @@ export function invLerp(a: number, b: number, t: number): number;
|
|||
// @public
|
||||
export function isDefined<T>(value: T): value is typeof value extends undefined ? never : T;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const isNativeStructuredClone: boolean;
|
||||
|
||||
// @public
|
||||
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;
|
||||
}>(a: T, b: T): -1 | 0 | 1;
|
||||
|
||||
// @internal
|
||||
export const STRUCTURED_CLONE_OBJECT_PROTOTYPE: any;
|
||||
|
||||
// @public
|
||||
const structuredClone_2: <T>(i: T) => T;
|
||||
export { structuredClone_2 as structuredClone }
|
||||
|
|
|
@ -357,72 +357,6 @@
|
|||
],
|
||||
"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",
|
||||
"canonicalReference": "@tldraw/utils!ErrorResult:type",
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest-environment-jsdom": "^29.4.3",
|
||||
"lazyrepo": "0.0.0-alpha.27"
|
||||
}
|
||||
}
|
||||
|
|
10
packages/utils/patchedJestJsDom.js
Normal file
10
packages/utils/patchedJestJsDom.js
Normal file
|
@ -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 {
|
||||
areObjectsShallowEqual,
|
||||
deepCopy,
|
||||
filterEntries,
|
||||
getOwnProperty,
|
||||
hasOwnProperty,
|
||||
|
@ -64,5 +63,12 @@ export {
|
|||
} from './lib/storage'
|
||||
export { fpsThrottle, throttleToNextFrame } from './lib/throttle'
|
||||
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'
|
||||
|
|
|
@ -19,40 +19,6 @@ export function getOwnProperty(obj: object, key: string): unknown {
|
|||
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.
|
||||
*
|
||||
|
|
|
@ -30,12 +30,44 @@ export function isNonNullish<T>(
|
|||
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()).
|
||||
*
|
||||
* @param i - The value to clone.
|
||||
* @public */
|
||||
export const structuredClone =
|
||||
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)
|
||||
export const structuredClone = _structuredClone[0]
|
||||
|
||||
/**
|
||||
* @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 {
|
||||
IndexKey,
|
||||
JsonValue,
|
||||
STRUCTURED_CLONE_OBJECT_PROTOTYPE,
|
||||
exhaustiveSwitchError,
|
||||
getOwnProperty,
|
||||
hasOwnProperty,
|
||||
|
@ -698,7 +699,9 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|||
return (
|
||||
typeof value === 'object' &&
|
||||
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'))
|
||||
|
||||
// construct the final package.json
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
const newManifest = structuredClone({
|
||||
// filter out comments
|
||||
...Object.fromEntries(
|
||||
|
|
|
@ -7188,6 +7188,7 @@ __metadata:
|
|||
"@microsoft/tsdoc": "npm:^0.14.2"
|
||||
"@radix-ui/react-accordion": "npm:^1.1.2"
|
||||
"@radix-ui/react-navigation-menu": "npm:^1.1.4"
|
||||
"@tldraw/utils": "workspace:*"
|
||||
"@types/broken-link-checker": "npm:^0.7.1"
|
||||
"@types/node": "npm:~20.11"
|
||||
"@types/sqlite3": "npm:^3.1.9"
|
||||
|
@ -7463,6 +7464,7 @@ __metadata:
|
|||
version: 0.0.0-use.local
|
||||
resolution: "@tldraw/utils@workspace:packages/utils"
|
||||
dependencies:
|
||||
jest-environment-jsdom: "npm:^29.4.3"
|
||||
lazyrepo: "npm:0.0.0-alpha.27"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
|
Loading…
Reference in a new issue