@tldraw/tlschema 4.2.2 → 4.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/dist-cjs/bindings/TLBaseBinding.js.map +2 -2
  2. package/dist-cjs/createTLSchema.js.map +2 -2
  3. package/dist-cjs/index.d.ts +71 -242
  4. package/dist-cjs/index.js +1 -4
  5. package/dist-cjs/index.js.map +2 -2
  6. package/dist-cjs/misc/TLOpacity.js +5 -1
  7. package/dist-cjs/misc/TLOpacity.js.map +2 -2
  8. package/dist-cjs/misc/TLRichText.js +1 -5
  9. package/dist-cjs/misc/TLRichText.js.map +2 -2
  10. package/dist-cjs/records/TLAsset.js.map +1 -1
  11. package/dist-cjs/records/TLBinding.js.map +2 -2
  12. package/dist-cjs/records/TLShape.js.map +2 -2
  13. package/dist-cjs/shapes/ShapeWithCrop.js.map +1 -1
  14. package/dist-cjs/shapes/TLArrowShape.js +13 -26
  15. package/dist-cjs/shapes/TLArrowShape.js.map +2 -2
  16. package/dist-cjs/shapes/TLBaseShape.js.map +2 -2
  17. package/dist-cjs/shapes/TLDrawShape.js +4 -37
  18. package/dist-cjs/shapes/TLDrawShape.js.map +2 -2
  19. package/dist-cjs/shapes/TLEmbedShape.js +0 -17
  20. package/dist-cjs/shapes/TLEmbedShape.js.map +2 -2
  21. package/dist-cjs/shapes/TLGeoShape.js +1 -12
  22. package/dist-cjs/shapes/TLGeoShape.js.map +2 -2
  23. package/dist-cjs/shapes/TLHighlightShape.js +2 -29
  24. package/dist-cjs/shapes/TLHighlightShape.js.map +2 -2
  25. package/dist-cjs/shapes/TLNoteShape.js +1 -12
  26. package/dist-cjs/shapes/TLNoteShape.js.map +2 -2
  27. package/dist-cjs/shapes/TLTextShape.js +1 -12
  28. package/dist-cjs/shapes/TLTextShape.js.map +2 -2
  29. package/dist-cjs/store-migrations.js +15 -15
  30. package/dist-cjs/store-migrations.js.map +2 -2
  31. package/dist-esm/bindings/TLBaseBinding.mjs.map +2 -2
  32. package/dist-esm/createTLSchema.mjs.map +2 -2
  33. package/dist-esm/index.d.mts +71 -242
  34. package/dist-esm/index.mjs +1 -5
  35. package/dist-esm/index.mjs.map +2 -2
  36. package/dist-esm/misc/TLOpacity.mjs +5 -1
  37. package/dist-esm/misc/TLOpacity.mjs.map +2 -2
  38. package/dist-esm/misc/TLRichText.mjs +1 -5
  39. package/dist-esm/misc/TLRichText.mjs.map +2 -2
  40. package/dist-esm/records/TLAsset.mjs.map +1 -1
  41. package/dist-esm/records/TLBinding.mjs.map +2 -2
  42. package/dist-esm/records/TLShape.mjs.map +2 -2
  43. package/dist-esm/shapes/TLArrowShape.mjs +13 -26
  44. package/dist-esm/shapes/TLArrowShape.mjs.map +2 -2
  45. package/dist-esm/shapes/TLBaseShape.mjs.map +2 -2
  46. package/dist-esm/shapes/TLDrawShape.mjs +4 -37
  47. package/dist-esm/shapes/TLDrawShape.mjs.map +2 -2
  48. package/dist-esm/shapes/TLEmbedShape.mjs +0 -17
  49. package/dist-esm/shapes/TLEmbedShape.mjs.map +2 -2
  50. package/dist-esm/shapes/TLGeoShape.mjs +1 -12
  51. package/dist-esm/shapes/TLGeoShape.mjs.map +2 -2
  52. package/dist-esm/shapes/TLHighlightShape.mjs +2 -29
  53. package/dist-esm/shapes/TLHighlightShape.mjs.map +2 -2
  54. package/dist-esm/shapes/TLNoteShape.mjs +1 -12
  55. package/dist-esm/shapes/TLNoteShape.mjs.map +2 -2
  56. package/dist-esm/shapes/TLTextShape.mjs +1 -12
  57. package/dist-esm/shapes/TLTextShape.mjs.map +2 -2
  58. package/dist-esm/store-migrations.mjs +15 -15
  59. package/dist-esm/store-migrations.mjs.map +2 -2
  60. package/package.json +8 -8
  61. package/src/__tests__/migrationTestUtils.ts +3 -9
  62. package/src/assets/TLBookmarkAsset.test.ts +96 -0
  63. package/src/assets/TLImageAsset.test.ts +213 -0
  64. package/src/assets/TLVideoAsset.test.ts +105 -0
  65. package/src/bindings/TLArrowBinding.test.ts +55 -0
  66. package/src/bindings/TLBaseBinding.ts +14 -25
  67. package/src/createTLSchema.ts +2 -8
  68. package/src/index.ts +0 -9
  69. package/src/migrations.test.ts +1 -149
  70. package/src/misc/TLOpacity.ts +5 -1
  71. package/src/misc/TLRichText.ts +1 -6
  72. package/src/misc/id-validator.test.ts +50 -0
  73. package/src/records/TLAsset.test.ts +234 -0
  74. package/src/records/TLAsset.ts +2 -2
  75. package/src/records/TLBinding.test.ts +22 -0
  76. package/src/records/TLBinding.ts +23 -65
  77. package/src/records/TLCamera.test.ts +19 -0
  78. package/src/records/TLDocument.test.ts +35 -0
  79. package/src/records/TLInstance.test.ts +201 -0
  80. package/src/records/TLPage.test.ts +110 -0
  81. package/src/records/TLPageState.test.ts +228 -0
  82. package/src/records/TLPointer.test.ts +63 -0
  83. package/src/records/TLPresence.test.ts +190 -0
  84. package/src/records/TLRecord.test.ts +70 -0
  85. package/src/records/TLShape.test.ts +232 -0
  86. package/src/records/TLShape.ts +5 -100
  87. package/src/shapes/ShapeWithCrop.test.ts +18 -0
  88. package/src/shapes/ShapeWithCrop.ts +2 -2
  89. package/src/shapes/TLArrowShape.test.ts +505 -0
  90. package/src/shapes/TLArrowShape.ts +14 -28
  91. package/src/shapes/TLBaseShape.test.ts +142 -0
  92. package/src/shapes/TLBaseShape.ts +10 -34
  93. package/src/shapes/TLBookmarkShape.test.ts +122 -0
  94. package/src/shapes/TLDrawShape.test.ts +177 -0
  95. package/src/shapes/TLDrawShape.ts +12 -59
  96. package/src/shapes/TLEmbedShape.test.ts +286 -0
  97. package/src/shapes/TLEmbedShape.ts +0 -17
  98. package/src/shapes/TLFrameShape.test.ts +71 -0
  99. package/src/shapes/TLGeoShape.test.ts +247 -0
  100. package/src/shapes/TLGeoShape.ts +1 -14
  101. package/src/shapes/TLGroupShape.test.ts +59 -0
  102. package/src/shapes/TLHighlightShape.test.ts +325 -0
  103. package/src/shapes/TLHighlightShape.ts +0 -37
  104. package/src/shapes/TLImageShape.test.ts +534 -0
  105. package/src/shapes/TLLineShape.test.ts +269 -0
  106. package/src/shapes/TLNoteShape.test.ts +1568 -0
  107. package/src/shapes/TLNoteShape.ts +1 -15
  108. package/src/shapes/TLTextShape.test.ts +407 -0
  109. package/src/shapes/TLTextShape.ts +2 -16
  110. package/src/shapes/TLVideoShape.test.ts +112 -0
  111. package/src/store-migrations.ts +16 -17
  112. package/src/styles/TLColorStyle.test.ts +439 -0
  113. package/dist-cjs/misc/b64Vecs.js +0 -224
  114. package/dist-cjs/misc/b64Vecs.js.map +0 -7
  115. package/dist-esm/misc/b64Vecs.mjs +0 -204
  116. package/dist-esm/misc/b64Vecs.mjs.map +0 -7
  117. package/src/misc/b64Vecs.ts +0 -308
package/src/index.ts CHANGED
@@ -99,8 +99,6 @@ export {
99
99
  type TLBindingId,
100
100
  type TLBindingUpdate,
101
101
  type TLDefaultBinding,
102
- type TLGlobalBindingPropsMap,
103
- type TLIndexedBindings,
104
102
  type TLUnknownBinding,
105
103
  } from './records/TLBinding'
106
104
  export { CameraRecordType, type TLCamera, type TLCameraId } from './records/TLCamera'
@@ -148,11 +146,7 @@ export {
148
146
  isShape,
149
147
  isShapeId,
150
148
  rootShapeMigrations,
151
- type ExtractShapeByProps,
152
- type TLCreateShapePartial,
153
149
  type TLDefaultShape,
154
- type TLGlobalShapePropsMap,
155
- type TLIndexedShapes,
156
150
  type TLParentId,
157
151
  type TLShape,
158
152
  type TLShapeId,
@@ -191,7 +185,6 @@ export {
191
185
  type TLBookmarkShapeProps,
192
186
  } from './shapes/TLBookmarkShape'
193
187
  export {
194
- compressLegacySegments,
195
188
  drawShapeMigrations,
196
189
  drawShapeProps,
197
190
  type TLDrawShape,
@@ -314,5 +307,3 @@ registerTldrawLibraryVersion(
314
307
  (globalThis as any).TLDRAW_LIBRARY_VERSION,
315
308
  (globalThis as any).TLDRAW_LIBRARY_MODULES
316
309
  )
317
-
318
- export { b64Vecs } from './misc/b64Vecs'
@@ -16,7 +16,7 @@ import { instancePresenceVersions } from './records/TLPresence'
16
16
  import { TLShape, rootShapeVersions } from './records/TLShape'
17
17
  import { arrowShapeVersions } from './shapes/TLArrowShape'
18
18
  import { bookmarkShapeVersions } from './shapes/TLBookmarkShape'
19
- import { compressLegacySegments, drawShapeVersions } from './shapes/TLDrawShape'
19
+ import { drawShapeVersions } from './shapes/TLDrawShape'
20
20
  import { embedShapeVersions } from './shapes/TLEmbedShape'
21
21
  import { frameShapeVersions } from './shapes/TLFrameShape'
22
22
  import { geoShapeVersions } from './shapes/TLGeoShape'
@@ -1444,32 +1444,6 @@ describe('Add rich text', () => {
1444
1444
  }
1445
1445
  })
1446
1446
 
1447
- describe('Add rich text attrs', () => {
1448
- const migrations = [
1449
- ['text shape', getTestMigration(textShapeVersions.AddRichTextAttrs)],
1450
- ['geo shape', getTestMigration(geoShapeVersions.AddRichTextAttrs)],
1451
- ['note shape', getTestMigration(noteShapeVersions.AddRichTextAttrs)],
1452
- ['arrow shape', getTestMigration(arrowShapeVersions.AddRichTextAttrs)],
1453
- ] as const
1454
-
1455
- for (const [shapeName, { up, down }] of migrations) {
1456
- it(`works for ${shapeName}`, () => {
1457
- const shape = { props: { richText: toRichText('hello, world') } }
1458
- const shapeWithAttrs = {
1459
- props: { richText: { ...toRichText('hello, world'), attrs: { test: 'value' } } },
1460
- }
1461
-
1462
- // Up migration should be a noop
1463
- expect(up(shape)).toEqual(shape)
1464
- expect(up(shapeWithAttrs)).toEqual(shapeWithAttrs)
1465
-
1466
- // Down migration should remove attrs
1467
- expect(down(shapeWithAttrs)).toEqual(shape)
1468
- expect(down(shape)).toEqual(shape)
1469
- })
1470
- }
1471
- })
1472
-
1473
1447
  describe('Make urls valid for all the assets', () => {
1474
1448
  const migrations = [
1475
1449
  ['bookmark asset', getTestMigration(bookmarkAssetVersions.MakeUrlsValid)],
@@ -2268,128 +2242,6 @@ describe('TLVideoAsset AddAutoplay', () => {
2268
2242
  })
2269
2243
  })
2270
2244
 
2271
- describe('Add scaleX, scaleY, and new base64 format to draw shape', () => {
2272
- const { up, down } = getTestMigration(drawShapeVersions.Base64)
2273
-
2274
- test('up works as expected', () => {
2275
- const legacySegments = [
2276
- {
2277
- type: 'free',
2278
- points: [
2279
- { x: 0, y: 0, z: 0.5 },
2280
- { x: 10, y: 10, z: 0.6 },
2281
- { x: 20, y: 20, z: 0.7 },
2282
- ],
2283
- },
2284
- {
2285
- type: 'straight',
2286
- points: [
2287
- { x: 20, y: 20, z: 0.7 },
2288
- { x: 30, y: 30, z: 0.8 },
2289
- ],
2290
- },
2291
- ]
2292
- expect(
2293
- up({
2294
- props: {
2295
- segments: legacySegments,
2296
- },
2297
- })
2298
- ).toEqual({
2299
- props: {
2300
- scaleX: 1,
2301
- scaleY: 1,
2302
- segments: compressLegacySegments(legacySegments as any),
2303
- },
2304
- })
2305
- })
2306
-
2307
- test('down works as expected', () => {
2308
- const legacySegments = [
2309
- {
2310
- type: 'free',
2311
- points: [
2312
- { x: 0, y: 0, z: 0.5 },
2313
- { x: 10, y: 10, z: 0.6 },
2314
- ],
2315
- },
2316
- ]
2317
- const compressed = compressLegacySegments(legacySegments as any)
2318
- const result = down({
2319
- props: {
2320
- scaleX: 1,
2321
- scaleY: 1,
2322
- segments: compressed,
2323
- },
2324
- })
2325
- expect(result.props.scaleX).toBeUndefined()
2326
- expect(result.props.scaleY).toBeUndefined()
2327
- expect(Array.isArray(result.props.segments[0].points)).toBe(true)
2328
- expect(result.props.segments[0].points.length).toBe(2)
2329
- })
2330
- })
2331
-
2332
- describe('Add scaleX, scaleY, and new base64 format to highlight shape', () => {
2333
- const { up, down } = getTestMigration(highlightShapeVersions.Base64)
2334
-
2335
- test('up works as expected', () => {
2336
- const legacySegments = [
2337
- {
2338
- type: 'free',
2339
- points: [
2340
- { x: 0, y: 0, z: 0.5 },
2341
- { x: 10, y: 10, z: 0.6 },
2342
- { x: 20, y: 20, z: 0.7 },
2343
- ],
2344
- },
2345
- {
2346
- type: 'straight',
2347
- points: [
2348
- { x: 20, y: 20, z: 0.7 },
2349
- { x: 30, y: 30, z: 0.8 },
2350
- ],
2351
- },
2352
- ]
2353
- expect(
2354
- up({
2355
- props: {
2356
- segments: legacySegments,
2357
- },
2358
- })
2359
- ).toEqual({
2360
- props: {
2361
- scaleX: 1,
2362
- scaleY: 1,
2363
- segments: compressLegacySegments(legacySegments as any),
2364
- },
2365
- })
2366
- })
2367
-
2368
- test('down works as expected', () => {
2369
- const legacySegments = [
2370
- {
2371
- type: 'free',
2372
- points: [
2373
- { x: 0, y: 0, z: 0.5 },
2374
- { x: 10, y: 10, z: 0.6 },
2375
- ],
2376
- },
2377
- ]
2378
- const compressed = compressLegacySegments(legacySegments as any)
2379
- const result = down({
2380
- props: {
2381
- scaleX: 1,
2382
- scaleY: 1,
2383
- segments: compressed,
2384
- },
2385
- })
2386
- expect(result.props.scaleX).toBeUndefined()
2387
- expect(result.props.scaleY).toBeUndefined()
2388
- expect(Array.isArray(result.props.segments[0].points)).toBe(true)
2389
- expect(result.props.segments[0].points.length).toBe(2)
2390
- })
2391
- })
2392
-
2393
2245
  /* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
2394
2246
 
2395
2247
  // check that all migrator fns were called at least once
@@ -53,4 +53,8 @@ export type TLOpacityType = number
53
53
  *
54
54
  * @public
55
55
  */
56
- export const opacityValidator = T.unitInterval
56
+ export const opacityValidator = T.number.check((n) => {
57
+ if (n < 0 || n > 1) {
58
+ throw new T.ValidationError('Opacity must be between 0 and 1')
59
+ }
60
+ })
@@ -12,12 +12,7 @@ import { T } from '@tldraw/validate'
12
12
  * const isValid = richTextValidator.check(richText) // true
13
13
  * ```
14
14
  */
15
-
16
- export const richTextValidator = T.object({
17
- type: T.string,
18
- content: T.arrayOf(T.unknown),
19
- attrs: T.any.optional(),
20
- })
15
+ export const richTextValidator = T.object({ type: T.string, content: T.arrayOf(T.unknown) })
21
16
 
22
17
  /**
23
18
  * Type representing rich text content in tldraw. Rich text follows a document-based
@@ -0,0 +1,50 @@
1
+ import type { RecordId, UnknownRecord } from '@tldraw/store'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { idValidator } from './id-validator'
4
+
5
+ // Mock record types for testing
6
+ interface MockShapeRecord extends UnknownRecord {
7
+ typeName: 'shape'
8
+ }
9
+
10
+ interface MockPageRecord extends UnknownRecord {
11
+ typeName: 'page'
12
+ }
13
+
14
+ type MockShapeId = RecordId<MockShapeRecord>
15
+ type MockPageId = RecordId<MockPageRecord>
16
+
17
+ describe('idValidator', () => {
18
+ it('should validate correct IDs with proper prefix', () => {
19
+ const shapeValidator = idValidator<MockShapeId>('shape')
20
+ const pageValidator = idValidator<MockPageId>('page')
21
+
22
+ expect(shapeValidator.validate('shape:abc123')).toBe('shape:abc123')
23
+ expect(shapeValidator.validate('shape:')).toBe('shape:')
24
+ expect(pageValidator.validate('page:main')).toBe('page:main')
25
+ })
26
+
27
+ it('should reject IDs with wrong prefix', () => {
28
+ const shapeValidator = idValidator<MockShapeId>('shape')
29
+
30
+ expect(() => shapeValidator.validate('page:abc123')).toThrow(
31
+ 'shape ID must start with "shape:"'
32
+ )
33
+ expect(() => shapeValidator.validate('asset:xyz789')).toThrow(
34
+ 'shape ID must start with "shape:"'
35
+ )
36
+ expect(() => shapeValidator.validate('abc123')).toThrow('shape ID must start with "shape:"')
37
+ expect(() => shapeValidator.validate('')).toThrow('shape ID must start with "shape:"')
38
+ })
39
+
40
+ it('should work with different prefixes independently', () => {
41
+ const shapeValidator = idValidator<MockShapeId>('shape')
42
+ const pageValidator = idValidator<MockPageId>('page')
43
+
44
+ expect(shapeValidator.isValid('shape:abc123')).toBe(true)
45
+ expect(shapeValidator.isValid('page:abc123')).toBe(false)
46
+
47
+ expect(pageValidator.isValid('shape:abc123')).toBe(false)
48
+ expect(pageValidator.isValid('page:abc123')).toBe(true)
49
+ })
50
+ })
@@ -0,0 +1,234 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getTestMigration } from '../__tests__/migrationTestUtils'
3
+ import {
4
+ assetMigrations,
5
+ AssetRecordType,
6
+ assetValidator,
7
+ assetVersions,
8
+ TLAssetId,
9
+ } from './TLAsset'
10
+
11
+ describe('TLAsset', () => {
12
+ describe('assetValidator', () => {
13
+ it('should validate different asset types using discriminated union', () => {
14
+ const imageAsset = {
15
+ id: 'asset:test_image',
16
+ typeName: 'asset' as const,
17
+ type: 'image' as const,
18
+ props: {
19
+ w: 100,
20
+ h: 100,
21
+ name: 'test.jpg',
22
+ isAnimated: false,
23
+ mimeType: 'image/jpeg',
24
+ src: 'https://example.com/test.jpg',
25
+ },
26
+ meta: {},
27
+ }
28
+
29
+ const videoAsset = {
30
+ id: 'asset:test_video',
31
+ typeName: 'asset' as const,
32
+ type: 'video' as const,
33
+ props: {
34
+ w: 640,
35
+ h: 480,
36
+ name: 'test.mp4',
37
+ isAnimated: true,
38
+ mimeType: 'video/mp4',
39
+ src: 'https://example.com/test.mp4',
40
+ },
41
+ meta: {},
42
+ }
43
+
44
+ const bookmarkAsset = {
45
+ id: 'asset:test_bookmark',
46
+ typeName: 'asset' as const,
47
+ type: 'bookmark' as const,
48
+ props: {
49
+ title: 'Test Site',
50
+ description: 'A test bookmark',
51
+ image: 'https://example.com/image.png',
52
+ favicon: 'https://example.com/favicon.ico',
53
+ src: 'https://example.com',
54
+ },
55
+ meta: {},
56
+ }
57
+
58
+ expect(() => assetValidator.validate(imageAsset)).not.toThrow()
59
+ expect(() => assetValidator.validate(videoAsset)).not.toThrow()
60
+ expect(() => assetValidator.validate(bookmarkAsset)).not.toThrow()
61
+
62
+ expect(assetValidator.validate(imageAsset).type).toBe('image')
63
+ expect(assetValidator.validate(videoAsset).type).toBe('video')
64
+ expect(assetValidator.validate(bookmarkAsset).type).toBe('bookmark')
65
+ })
66
+
67
+ it('should reject invalid asset types', () => {
68
+ const invalidAsset = {
69
+ id: 'asset:invalid',
70
+ typeName: 'asset' as const,
71
+ type: 'invalid_type' as any,
72
+ props: {
73
+ w: 100,
74
+ h: 100,
75
+ name: 'test.jpg',
76
+ },
77
+ meta: {},
78
+ }
79
+
80
+ expect(() => assetValidator.validate(invalidAsset)).toThrow()
81
+ })
82
+
83
+ it('should require valid asset structure', () => {
84
+ // Wrong typeName
85
+ const wrongTypeName = {
86
+ id: 'asset:wrong_typename',
87
+ typeName: 'shape' as const,
88
+ type: 'image' as const,
89
+ props: {
90
+ w: 100,
91
+ h: 100,
92
+ name: 'test.jpg',
93
+ isAnimated: false,
94
+ mimeType: 'image/jpeg',
95
+ src: 'https://example.com/test.jpg',
96
+ },
97
+ meta: {},
98
+ }
99
+ expect(() => assetValidator.validate(wrongTypeName)).toThrow()
100
+
101
+ // Missing meta
102
+ const noMeta = {
103
+ id: 'asset:no_meta',
104
+ typeName: 'asset' as const,
105
+ type: 'image' as const,
106
+ props: {
107
+ w: 100,
108
+ h: 100,
109
+ name: 'test.jpg',
110
+ isAnimated: false,
111
+ mimeType: 'image/jpeg',
112
+ src: 'https://example.com/test.jpg',
113
+ },
114
+ }
115
+ expect(() => assetValidator.validate(noMeta)).toThrow()
116
+
117
+ // Wrong ID prefix
118
+ const wrongId = {
119
+ id: 'shape:wrong_prefix',
120
+ typeName: 'asset' as const,
121
+ type: 'image' as const,
122
+ props: {
123
+ w: 100,
124
+ h: 100,
125
+ name: 'test.jpg',
126
+ isAnimated: false,
127
+ mimeType: 'image/jpeg',
128
+ src: 'https://example.com/test.jpg',
129
+ },
130
+ meta: {},
131
+ }
132
+ expect(() => assetValidator.validate(wrongId)).toThrow()
133
+ })
134
+ })
135
+
136
+ describe('assetMigrations', () => {
137
+ it('should have correct migration structure', () => {
138
+ expect(assetMigrations.sequenceId).toBe('com.tldraw.asset')
139
+ expect(assetMigrations.sequence).toHaveLength(1)
140
+ expect(assetMigrations.sequence[0].id).toBe(assetVersions.AddMeta)
141
+ })
142
+ })
143
+
144
+ describe('AddMeta migration', () => {
145
+ const { up } = getTestMigration(assetVersions.AddMeta)
146
+
147
+ it('should add empty meta property', () => {
148
+ const assetWithoutMeta = {
149
+ id: 'asset:test',
150
+ typeName: 'asset',
151
+ type: 'image',
152
+ props: {
153
+ w: 100,
154
+ h: 100,
155
+ name: 'test.jpg',
156
+ isAnimated: false,
157
+ mimeType: 'image/jpeg',
158
+ src: 'https://example.com/test.jpg',
159
+ },
160
+ }
161
+
162
+ const result = up(assetWithoutMeta)
163
+ expect(result.meta).toEqual({})
164
+ expect(result.props).toEqual(assetWithoutMeta.props)
165
+ })
166
+
167
+ it('should overwrite existing meta property', () => {
168
+ const assetWithMeta = {
169
+ id: 'asset:test',
170
+ typeName: 'asset',
171
+ type: 'image',
172
+ props: {
173
+ w: 100,
174
+ h: 100,
175
+ name: 'test.jpg',
176
+ isAnimated: false,
177
+ mimeType: 'image/jpeg',
178
+ src: 'https://example.com/test.jpg',
179
+ },
180
+ meta: { existing: 'data' },
181
+ }
182
+
183
+ const result = up(assetWithMeta)
184
+ expect(result.meta).toEqual({}) // Migration always sets to empty object
185
+ })
186
+
187
+ it('should preserve other properties during migration', () => {
188
+ const assetWithExtraProps = {
189
+ id: 'asset:extra_props',
190
+ typeName: 'asset',
191
+ type: 'image',
192
+ props: {
193
+ w: 200,
194
+ h: 150,
195
+ name: 'extra.png',
196
+ isAnimated: false,
197
+ mimeType: 'image/png',
198
+ src: 'https://example.com/extra.png',
199
+ },
200
+ customProperty: 'should be preserved',
201
+ }
202
+
203
+ const result = up(assetWithExtraProps)
204
+ expect(result.meta).toEqual({})
205
+ expect(result.customProperty).toBe('should be preserved')
206
+ })
207
+ })
208
+
209
+ describe('AssetRecordType', () => {
210
+ it('should have correct configuration', () => {
211
+ expect(AssetRecordType.typeName).toBe('asset')
212
+ expect(AssetRecordType.scope).toBe('document')
213
+ expect(AssetRecordType.validator).toBe(assetValidator)
214
+ })
215
+
216
+ it('should create records with default meta property', () => {
217
+ const assetRecord = AssetRecordType.create({
218
+ id: 'asset:test' as TLAssetId,
219
+ type: 'image',
220
+ props: {
221
+ w: 100,
222
+ h: 100,
223
+ name: 'test.jpg',
224
+ isAnimated: false,
225
+ mimeType: 'image/jpeg',
226
+ src: 'https://example.com/test.jpg',
227
+ },
228
+ })
229
+
230
+ expect(assetRecord.meta).toEqual({})
231
+ expect(assetRecord.typeName).toBe('asset')
232
+ })
233
+ })
234
+ })
@@ -9,7 +9,7 @@ import { TLBaseAsset } from '../assets/TLBaseAsset'
9
9
  import { bookmarkAssetValidator, TLBookmarkAsset } from '../assets/TLBookmarkAsset'
10
10
  import { imageAssetValidator, TLImageAsset } from '../assets/TLImageAsset'
11
11
  import { TLVideoAsset, videoAssetValidator } from '../assets/TLVideoAsset'
12
- import { ExtractShapeByProps } from './TLShape'
12
+ import { TLShape } from './TLShape'
13
13
 
14
14
  /**
15
15
  * Union type representing all possible asset types in tldraw.
@@ -222,4 +222,4 @@ export type TLAssetId = RecordId<TLBaseAsset<any, any>>
222
222
  *
223
223
  * @public
224
224
  */
225
- export type TLAssetShape = ExtractShapeByProps<{ assetId: TLAssetId }>
225
+ export type TLAssetShape = Extract<TLShape, { props: { assetId: TLAssetId } }>
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createBindingId, rootBindingMigrations } from './TLBinding'
3
+
4
+ describe('TLBinding', () => {
5
+ describe('createBindingId function', () => {
6
+ it('should generate IDs starting with binding:', () => {
7
+ const id = createBindingId()
8
+ expect(id.startsWith('binding:')).toBe(true)
9
+ })
10
+
11
+ it('should use custom ID when provided', () => {
12
+ expect(createBindingId('test')).toBe('binding:test')
13
+ })
14
+ })
15
+
16
+ describe('rootBindingMigrations', () => {
17
+ it('should have correct structure', () => {
18
+ expect(rootBindingMigrations.sequenceId).toBe('com.tldraw.binding')
19
+ expect(Array.isArray(rootBindingMigrations.sequence)).toBe(true)
20
+ })
21
+ })
22
+ })
@@ -5,7 +5,7 @@ import {
5
5
  createRecordMigrationSequence,
6
6
  createRecordType,
7
7
  } from '@tldraw/store'
8
- import { mapObjectMapValues, uniqueId } from '@tldraw/utils'
8
+ import { Expand, mapObjectMapValues, uniqueId } from '@tldraw/utils'
9
9
  import { T } from '@tldraw/validate'
10
10
  import { TLArrowBinding } from '../bindings/TLArrowBinding'
11
11
  import { TLBaseBinding, createBindingValidator } from '../bindings/TLBaseBinding'
@@ -56,42 +56,10 @@ export type TLDefaultBinding = TLArrowBinding
56
56
  */
57
57
  export type TLUnknownBinding = TLBaseBinding<string, object>
58
58
 
59
- /** @public */
60
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
61
- export interface TLGlobalBindingPropsMap {}
62
-
63
- /** @public */
64
- // prettier-ignore
65
- export type TLIndexedBindings = {
66
- // We iterate over a union of augmented keys and default binding types.
67
- // This allows us to include (or conditionally exclude or override) the default bindings in one go.
68
- //
69
- // In the `as` clause we are filtering out disabled bindings.
70
- [K in keyof TLGlobalBindingPropsMap | TLDefaultBinding['type'] as K extends TLDefaultBinding['type']
71
- ? K extends keyof TLGlobalBindingPropsMap
72
- ? // if it extends a nullish value the user has disabled this binding type so we filter it out with never
73
- TLGlobalBindingPropsMap[K] extends null | undefined
74
- ? never
75
- : K
76
- : K
77
- : K]: K extends TLDefaultBinding['type']
78
- ? // if it's a default binding type we need to check if it's been overridden
79
- K extends keyof TLGlobalBindingPropsMap
80
- ? // if it has been overriden then use the custom binding definition
81
- TLBaseBinding<K, TLGlobalBindingPropsMap[K]>
82
- : // if it has not been overriden then reuse existing type aliases for better type display
83
- Extract<TLDefaultBinding, { type: K }>
84
- : // use the custom binding definition
85
- TLBaseBinding<K, TLGlobalBindingPropsMap[K & keyof TLGlobalBindingPropsMap]>
86
- }
87
-
88
59
  /**
89
- * The set of all bindings that are available in the editor.
60
+ * The set of all bindings that are available in the editor, including unknown bindings.
90
61
  * Bindings represent relationships between shapes, such as arrows connecting to other shapes.
91
62
  *
92
- * You can use this type without a type argument to work with any binding, or pass
93
- * a specific binding type string (e.g., `'arrow'`) to narrow down to that specific binding type.
94
- *
95
63
  * @example
96
64
  * ```ts
97
65
  * // Check binding type and handle accordingly
@@ -105,17 +73,11 @@ export type TLIndexedBindings = {
105
73
  * break
106
74
  * }
107
75
  * }
108
- *
109
- * // Narrow to a specific binding type by passing the type as a generic argument
110
- * function getArrowSourceId(binding: TLBinding<'arrow'>) {
111
- * return binding.fromId // TypeScript knows this is a TLArrowBinding
112
- * }
113
76
  * ```
114
77
  *
115
78
  * @public
116
79
  */
117
- export type TLBinding<K extends keyof TLIndexedBindings = keyof TLIndexedBindings> =
118
- TLIndexedBindings[K]
80
+ export type TLBinding = TLDefaultBinding | TLUnknownBinding
119
81
 
120
82
  /**
121
83
  * Type for updating existing bindings with partial properties.
@@ -137,17 +99,15 @@ export type TLBinding<K extends keyof TLIndexedBindings = keyof TLIndexedBinding
137
99
  *
138
100
  * @public
139
101
  */
140
- export type TLBindingUpdate<T extends TLBinding = TLBinding> = T extends T
141
- ? {
142
- id: TLBindingId
143
- type: T['type']
144
- typeName?: T['typeName']
145
- fromId?: T['fromId']
146
- toId?: T['toId']
147
- props?: Partial<T['props']>
148
- meta?: Partial<T['meta']>
149
- }
150
- : never
102
+ export type TLBindingUpdate<T extends TLBinding = TLBinding> = Expand<{
103
+ id: TLBindingId
104
+ type: T['type']
105
+ typeName?: T['typeName']
106
+ fromId?: T['fromId']
107
+ toId?: T['toId']
108
+ props?: Partial<T['props']>
109
+ meta?: Partial<T['meta']>
110
+ }>
151
111
 
152
112
  /**
153
113
  * Type for creating new bindings with required fromId and toId.
@@ -173,17 +133,15 @@ export type TLBindingUpdate<T extends TLBinding = TLBinding> = T extends T
173
133
  *
174
134
  * @public
175
135
  */
176
- export type TLBindingCreate<T extends TLBinding = TLBinding> = T extends T
177
- ? {
178
- id?: TLBindingId
179
- type: T['type']
180
- typeName?: T['typeName']
181
- fromId: T['fromId']
182
- toId: T['toId']
183
- props?: Partial<T['props']>
184
- meta?: Partial<T['meta']>
185
- }
186
- : never
136
+ export type TLBindingCreate<T extends TLBinding = TLBinding> = Expand<{
137
+ id?: TLBindingId
138
+ type: T['type']
139
+ typeName?: T['typeName']
140
+ fromId: T['fromId']
141
+ toId: T['toId']
142
+ props?: Partial<T['props']>
143
+ meta?: Partial<T['meta']>
144
+ }>
187
145
 
188
146
  /**
189
147
  * Branded string type for binding record identifiers.
@@ -208,7 +166,7 @@ export type TLBindingCreate<T extends TLBinding = TLBinding> = T extends T
208
166
  *
209
167
  * @public
210
168
  */
211
- export type TLBindingId = RecordId<TLBinding>
169
+ export type TLBindingId = RecordId<TLUnknownBinding>
212
170
 
213
171
  /**
214
172
  * Migration version identifiers for the root binding record schema.
@@ -417,7 +375,7 @@ export function createBindingPropsMigrationIds<S extends string, T extends Recor
417
375
  * @internal
418
376
  */
419
377
  export function createBindingRecordType(bindings: Record<string, SchemaPropsInfo>) {
420
- return createRecordType('binding', {
378
+ return createRecordType<TLBinding>('binding', {
421
379
  scope: 'document',
422
380
  validator: T.model(
423
381
  'binding',