@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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/store-migrations.ts"],
4
- "sourcesContent": ["import { createMigrationIds, createMigrationSequence } from '@tldraw/store'\nimport { IndexKey, objectMapEntries } from '@tldraw/utils'\nimport { TLPage } from './records/TLPage'\nimport { TLShape } from './records/TLShape'\nimport { TLLineShape } from './shapes/TLLineShape'\n\n/**\n * Migration version constants for store-level schema changes.\n * Each version represents a breaking change that requires data transformation.\n *\n * @internal\n */\nconst Versions = createMigrationIds('com.tldraw.store', {\n\tRemoveCodeAndIconShapeTypes: 1,\n\tAddInstancePresenceType: 2,\n\tRemoveTLUserAndPresenceAndAddPointer: 3,\n\tRemoveUserDocument: 4,\n\tFixIndexKeys: 5,\n} as const)\n\n/**\n * Migration version identifiers for store-level migrations.\n * These versions track changes to the overall store structure and data model.\n *\n * @example\n * ```ts\n * import { storeVersions } from '@tldraw/tlschema'\n *\n * // Check if a specific migration version exists\n * const hasRemoveCodeShapes = storeVersions.RemoveCodeAndIconShapeTypes\n * ```\n *\n * @public\n */\nexport { Versions as storeVersions }\n\n/**\n * Store-level migration sequence that handles evolution of the tldraw data model.\n * These migrations run when the store schema version changes and ensure backward\n * compatibility by transforming old data structures to new formats.\n *\n * The migrations handle:\n * - Removal of deprecated shape types (code, icon)\n * - Addition of new record types (instance presence)\n * - Cleanup of obsolete user and presence data\n * - Removal of deprecated user document records\n *\n * @example\n * ```ts\n * import { storeMigrations } from '@tldraw/tlschema'\n * import { migrate } from '@tldraw/store'\n *\n * // Apply store migrations to old data\n * const migratedStore = migrate({\n * store: oldStoreData,\n * migrations: storeMigrations,\n * fromVersion: 0,\n * toVersion: storeMigrations.currentVersion\n * })\n * ```\n *\n * @public\n */\nexport const storeMigrations = createMigrationSequence({\n\tsequenceId: 'com.tldraw.store',\n\tretroactive: false,\n\tsequence: [\n\t\t{\n\t\t\tid: Versions.RemoveCodeAndIconShapeTypes,\n\t\t\tscope: 'storage',\n\t\t\tup: (storage) => {\n\t\t\t\tfor (const [id, record] of storage.entries()) {\n\t\t\t\t\tif (\n\t\t\t\t\t\trecord.typeName === 'shape' &&\n\t\t\t\t\t\t'type' in record &&\n\t\t\t\t\t\t(record.type === 'icon' || record.type === 'code')\n\t\t\t\t\t) {\n\t\t\t\t\t\tstorage.delete(id)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddInstancePresenceType,\n\t\t\tscope: 'storage',\n\t\t\tup(_storage) {\n\t\t\t\t// noop\n\t\t\t\t// there used to be a down migration for this but we made down migrations optional\n\t\t\t\t// and we don't use them on storage-level migrations so we can just remove it\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// remove user and presence records and add pointer records\n\t\t\tid: Versions.RemoveTLUserAndPresenceAndAddPointer,\n\t\t\tscope: 'storage',\n\t\t\tup: (storage) => {\n\t\t\t\tfor (const [id, record] of storage.entries()) {\n\t\t\t\t\tif (record.typeName.match(/^(user|user_presence)$/)) {\n\t\t\t\t\t\tstorage.delete(id)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// remove user document records\n\t\t\tid: Versions.RemoveUserDocument,\n\t\t\tscope: 'storage',\n\t\t\tup: (storage) => {\n\t\t\t\tfor (const [id, record] of storage.entries()) {\n\t\t\t\t\tif (record.typeName.match('user_document')) {\n\t\t\t\t\t\tstorage.delete(id)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.FixIndexKeys,\n\t\t\tscope: 'record',\n\t\t\tup: (record) => {\n\t\t\t\tif (['shape', 'page'].includes(record.typeName) && 'index' in record) {\n\t\t\t\t\tconst recordWithIndex = record as TLShape | TLPage\n\t\t\t\t\t// Our newer fractional indexed library (more correctly) validates that indices\n\t\t\t\t\t// do not end with 0. ('a0' being an exception)\n\t\t\t\t\tif (recordWithIndex.index.endsWith('0') && recordWithIndex.index !== 'a0') {\n\t\t\t\t\t\trecordWithIndex.index = (recordWithIndex.index.slice(0, -1) +\n\t\t\t\t\t\t\tgetNRandomBase62Digits(3)) as IndexKey\n\t\t\t\t\t}\n\t\t\t\t\t// Line shapes have 'points' that have indices as well.\n\t\t\t\t\tif (record.typeName === 'shape' && (recordWithIndex as TLShape).type === 'line') {\n\t\t\t\t\t\tconst lineShape = recordWithIndex as TLLineShape\n\t\t\t\t\t\tfor (const [_, point] of objectMapEntries(lineShape.props.points)) {\n\t\t\t\t\t\t\tif (point.index.endsWith('0') && point.index !== 'a0') {\n\t\t\t\t\t\t\t\tpoint.index = (point.index.slice(0, -1) + getNRandomBase62Digits(3)) as IndexKey\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\tdown: () => {\n\t\t\t\t// noop\n\t\t\t\t// Enables tlsync to support older clients so as to not force people to refresh immediately after deploying.\n\t\t\t},\n\t\t},\n\t],\n})\n\nconst BASE_62_DIGITS_WITHOUT_ZERO = '123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'\nconst getRandomBase62Digit = () => {\n\treturn BASE_62_DIGITS_WITHOUT_ZERO.charAt(\n\t\tMath.floor(Math.random() * BASE_62_DIGITS_WITHOUT_ZERO.length)\n\t)\n}\n\nconst getNRandomBase62Digits = (n: number) => {\n\treturn Array.from({ length: n }, getRandomBase62Digit).join('')\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB,+BAA+B;AAC5D,SAAmB,wBAAwB;AAW3C,MAAM,WAAW,mBAAmB,oBAAoB;AAAA,EACvD,6BAA6B;AAAA,EAC7B,yBAAyB;AAAA,EACzB,sCAAsC;AAAA,EACtC,oBAAoB;AAAA,EACpB,cAAc;AACf,CAAU;AA6CH,MAAM,kBAAkB,wBAAwB;AAAA,EACtD,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,UAAU;AAAA,IACT;AAAA,MACC,IAAI,SAAS;AAAA,MACb,OAAO;AAAA,MACP,IAAI,CAAC,YAAY;AAChB,mBAAW,CAAC,IAAI,MAAM,KAAK,QAAQ,QAAQ,GAAG;AAC7C,cACC,OAAO,aAAa,WACpB,UAAU,WACT,OAAO,SAAS,UAAU,OAAO,SAAS,SAC1C;AACD,oBAAQ,OAAO,EAAE;AAAA,UAClB;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI,SAAS;AAAA,MACb,OAAO;AAAA,MACP,GAAG,UAAU;AAAA,MAIb;AAAA,IACD;AAAA,IACA;AAAA;AAAA,MAEC,IAAI,SAAS;AAAA,MACb,OAAO;AAAA,MACP,IAAI,CAAC,YAAY;AAChB,mBAAW,CAAC,IAAI,MAAM,KAAK,QAAQ,QAAQ,GAAG;AAC7C,cAAI,OAAO,SAAS,MAAM,wBAAwB,GAAG;AACpD,oBAAQ,OAAO,EAAE;AAAA,UAClB;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,IACA;AAAA;AAAA,MAEC,IAAI,SAAS;AAAA,MACb,OAAO;AAAA,MACP,IAAI,CAAC,YAAY;AAChB,mBAAW,CAAC,IAAI,MAAM,KAAK,QAAQ,QAAQ,GAAG;AAC7C,cAAI,OAAO,SAAS,MAAM,eAAe,GAAG;AAC3C,oBAAQ,OAAO,EAAE;AAAA,UAClB;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI,SAAS;AAAA,MACb,OAAO;AAAA,MACP,IAAI,CAAC,WAAW;AACf,YAAI,CAAC,SAAS,MAAM,EAAE,SAAS,OAAO,QAAQ,KAAK,WAAW,QAAQ;AACrE,gBAAM,kBAAkB;AAGxB,cAAI,gBAAgB,MAAM,SAAS,GAAG,KAAK,gBAAgB,UAAU,MAAM;AAC1E,4BAAgB,QAAS,gBAAgB,MAAM,MAAM,GAAG,EAAE,IACzD,uBAAuB,CAAC;AAAA,UAC1B;AAEA,cAAI,OAAO,aAAa,WAAY,gBAA4B,SAAS,QAAQ;AAChF,kBAAM,YAAY;AAClB,uBAAW,CAAC,GAAG,KAAK,KAAK,iBAAiB,UAAU,MAAM,MAAM,GAAG;AAClE,kBAAI,MAAM,MAAM,SAAS,GAAG,KAAK,MAAM,UAAU,MAAM;AACtD,sBAAM,QAAS,MAAM,MAAM,MAAM,GAAG,EAAE,IAAI,uBAAuB,CAAC;AAAA,cACnE;AAAA,YACD;AAAA,UACD;AAAA,QACD;AAAA,MACD;AAAA,MACA,MAAM,MAAM;AAAA,MAGZ;AAAA,IACD;AAAA,EACD;AACD,CAAC;AAED,MAAM,8BAA8B;AACpC,MAAM,uBAAuB,MAAM;AAClC,SAAO,4BAA4B;AAAA,IAClC,KAAK,MAAM,KAAK,OAAO,IAAI,4BAA4B,MAAM;AAAA,EAC9D;AACD;AAEA,MAAM,yBAAyB,CAAC,MAAc;AAC7C,SAAO,MAAM,KAAK,EAAE,QAAQ,EAAE,GAAG,oBAAoB,EAAE,KAAK,EAAE;AAC/D;",
4
+ "sourcesContent": ["import { createMigrationIds, createMigrationSequence } from '@tldraw/store'\nimport { IndexKey, objectMapEntries } from '@tldraw/utils'\nimport { TLPage } from './records/TLPage'\nimport { TLShape } from './records/TLShape'\nimport { TLLineShape } from './shapes/TLLineShape'\n\n/**\n * Migration version constants for store-level schema changes.\n * Each version represents a breaking change that requires data transformation.\n *\n * @internal\n */\nconst Versions = createMigrationIds('com.tldraw.store', {\n\tRemoveCodeAndIconShapeTypes: 1,\n\tAddInstancePresenceType: 2,\n\tRemoveTLUserAndPresenceAndAddPointer: 3,\n\tRemoveUserDocument: 4,\n\tFixIndexKeys: 5,\n} as const)\n\n/**\n * Migration version identifiers for store-level migrations.\n * These versions track changes to the overall store structure and data model.\n *\n * @example\n * ```ts\n * import { storeVersions } from '@tldraw/tlschema'\n *\n * // Check if a specific migration version exists\n * const hasRemoveCodeShapes = storeVersions.RemoveCodeAndIconShapeTypes\n * ```\n *\n * @public\n */\nexport { Versions as storeVersions }\n\n/**\n * Store-level migration sequence that handles evolution of the tldraw data model.\n * These migrations run when the store schema version changes and ensure backward\n * compatibility by transforming old data structures to new formats.\n *\n * The migrations handle:\n * - Removal of deprecated shape types (code, icon)\n * - Addition of new record types (instance presence)\n * - Cleanup of obsolete user and presence data\n * - Removal of deprecated user document records\n *\n * @example\n * ```ts\n * import { storeMigrations } from '@tldraw/tlschema'\n * import { migrate } from '@tldraw/store'\n *\n * // Apply store migrations to old data\n * const migratedStore = migrate({\n * store: oldStoreData,\n * migrations: storeMigrations,\n * fromVersion: 0,\n * toVersion: storeMigrations.currentVersion\n * })\n * ```\n *\n * @public\n */\nexport const storeMigrations = createMigrationSequence({\n\tsequenceId: 'com.tldraw.store',\n\tretroactive: false,\n\tsequence: [\n\t\t{\n\t\t\tid: Versions.RemoveCodeAndIconShapeTypes,\n\t\t\tscope: 'store',\n\t\t\tup: (store) => {\n\t\t\t\tfor (const [id, record] of objectMapEntries(store)) {\n\t\t\t\t\tif (\n\t\t\t\t\t\trecord.typeName === 'shape' &&\n\t\t\t\t\t\t((record as TLShape).type === 'icon' || (record as TLShape).type === 'code')\n\t\t\t\t\t) {\n\t\t\t\t\t\tdelete store[id]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddInstancePresenceType,\n\t\t\tscope: 'store',\n\t\t\tup(_store) {\n\t\t\t\t// noop\n\t\t\t\t// there used to be a down migration for this but we made down migrations optional\n\t\t\t\t// and we don't use them on store-level migrations so we can just remove it\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// remove user and presence records and add pointer records\n\t\t\tid: Versions.RemoveTLUserAndPresenceAndAddPointer,\n\t\t\tscope: 'store',\n\t\t\tup: (store) => {\n\t\t\t\tfor (const [id, record] of objectMapEntries(store)) {\n\t\t\t\t\tif (record.typeName.match(/^(user|user_presence)$/)) {\n\t\t\t\t\t\tdelete store[id]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// remove user document records\n\t\t\tid: Versions.RemoveUserDocument,\n\t\t\tscope: 'store',\n\t\t\tup: (store) => {\n\t\t\t\tfor (const [id, record] of objectMapEntries(store)) {\n\t\t\t\t\tif (record.typeName.match('user_document')) {\n\t\t\t\t\t\tdelete store[id]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.FixIndexKeys,\n\t\t\tscope: 'record',\n\t\t\tup: (record) => {\n\t\t\t\tif (['shape', 'page'].includes(record.typeName) && 'index' in record) {\n\t\t\t\t\tconst recordWithIndex = record as TLShape | TLPage\n\t\t\t\t\t// Our newer fractional indexed library (more correctly) validates that indices\n\t\t\t\t\t// do not end with 0. ('a0' being an exception)\n\t\t\t\t\tif (recordWithIndex.index.endsWith('0') && recordWithIndex.index !== 'a0') {\n\t\t\t\t\t\trecordWithIndex.index = (recordWithIndex.index.slice(0, -1) +\n\t\t\t\t\t\t\tgetNRandomBase62Digits(3)) as IndexKey\n\t\t\t\t\t}\n\t\t\t\t\t// Line shapes have 'points' that have indices as well.\n\t\t\t\t\tif (record.typeName === 'shape' && (recordWithIndex as TLShape).type === 'line') {\n\t\t\t\t\t\tconst lineShape = recordWithIndex as TLLineShape\n\t\t\t\t\t\tfor (const [_, point] of objectMapEntries(lineShape.props.points)) {\n\t\t\t\t\t\t\tif (point.index.endsWith('0') && point.index !== 'a0') {\n\t\t\t\t\t\t\t\tpoint.index = (point.index.slice(0, -1) + getNRandomBase62Digits(3)) as IndexKey\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\tdown: () => {\n\t\t\t\t// noop\n\t\t\t\t// Enables tlsync to support older clients so as to not force people to refresh immediately after deploying.\n\t\t\t},\n\t\t},\n\t],\n})\n\nconst BASE_62_DIGITS_WITHOUT_ZERO = '123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'\nconst getRandomBase62Digit = () => {\n\treturn BASE_62_DIGITS_WITHOUT_ZERO.charAt(\n\t\tMath.floor(Math.random() * BASE_62_DIGITS_WITHOUT_ZERO.length)\n\t)\n}\n\nconst getNRandomBase62Digits = (n: number) => {\n\treturn Array.from({ length: n }, getRandomBase62Digit).join('')\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB,+BAA+B;AAC5D,SAAmB,wBAAwB;AAW3C,MAAM,WAAW,mBAAmB,oBAAoB;AAAA,EACvD,6BAA6B;AAAA,EAC7B,yBAAyB;AAAA,EACzB,sCAAsC;AAAA,EACtC,oBAAoB;AAAA,EACpB,cAAc;AACf,CAAU;AA6CH,MAAM,kBAAkB,wBAAwB;AAAA,EACtD,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,UAAU;AAAA,IACT;AAAA,MACC,IAAI,SAAS;AAAA,MACb,OAAO;AAAA,MACP,IAAI,CAAC,UAAU;AACd,mBAAW,CAAC,IAAI,MAAM,KAAK,iBAAiB,KAAK,GAAG;AACnD,cACC,OAAO,aAAa,YAClB,OAAmB,SAAS,UAAW,OAAmB,SAAS,SACpE;AACD,mBAAO,MAAM,EAAE;AAAA,UAChB;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI,SAAS;AAAA,MACb,OAAO;AAAA,MACP,GAAG,QAAQ;AAAA,MAIX;AAAA,IACD;AAAA,IACA;AAAA;AAAA,MAEC,IAAI,SAAS;AAAA,MACb,OAAO;AAAA,MACP,IAAI,CAAC,UAAU;AACd,mBAAW,CAAC,IAAI,MAAM,KAAK,iBAAiB,KAAK,GAAG;AACnD,cAAI,OAAO,SAAS,MAAM,wBAAwB,GAAG;AACpD,mBAAO,MAAM,EAAE;AAAA,UAChB;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,IACA;AAAA;AAAA,MAEC,IAAI,SAAS;AAAA,MACb,OAAO;AAAA,MACP,IAAI,CAAC,UAAU;AACd,mBAAW,CAAC,IAAI,MAAM,KAAK,iBAAiB,KAAK,GAAG;AACnD,cAAI,OAAO,SAAS,MAAM,eAAe,GAAG;AAC3C,mBAAO,MAAM,EAAE;AAAA,UAChB;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI,SAAS;AAAA,MACb,OAAO;AAAA,MACP,IAAI,CAAC,WAAW;AACf,YAAI,CAAC,SAAS,MAAM,EAAE,SAAS,OAAO,QAAQ,KAAK,WAAW,QAAQ;AACrE,gBAAM,kBAAkB;AAGxB,cAAI,gBAAgB,MAAM,SAAS,GAAG,KAAK,gBAAgB,UAAU,MAAM;AAC1E,4BAAgB,QAAS,gBAAgB,MAAM,MAAM,GAAG,EAAE,IACzD,uBAAuB,CAAC;AAAA,UAC1B;AAEA,cAAI,OAAO,aAAa,WAAY,gBAA4B,SAAS,QAAQ;AAChF,kBAAM,YAAY;AAClB,uBAAW,CAAC,GAAG,KAAK,KAAK,iBAAiB,UAAU,MAAM,MAAM,GAAG;AAClE,kBAAI,MAAM,MAAM,SAAS,GAAG,KAAK,MAAM,UAAU,MAAM;AACtD,sBAAM,QAAS,MAAM,MAAM,MAAM,GAAG,EAAE,IAAI,uBAAuB,CAAC;AAAA,cACnE;AAAA,YACD;AAAA,UACD;AAAA,QACD;AAAA,MACD;AAAA,MACA,MAAM,MAAM;AAAA,MAGZ;AAAA,IACD;AAAA,EACD;AACD,CAAC;AAED,MAAM,8BAA8B;AACpC,MAAM,uBAAuB,MAAM;AAClC,SAAO,4BAA4B;AAAA,IAClC,KAAK,MAAM,KAAK,OAAO,IAAI,4BAA4B,MAAM;AAAA,EAC9D;AACD;AAEA,MAAM,yBAAyB,CAAC,MAAc;AAC7C,SAAO,MAAM,KAAK,EAAE,QAAQ,EAAE,GAAG,oBAAoB,EAAE,KAAK,EAAE;AAC/D;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/tlschema",
3
3
  "description": "tldraw infinite canvas SDK (schema).",
4
- "version": "4.2.2",
4
+ "version": "4.2.3",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -47,18 +47,18 @@
47
47
  "devDependencies": {
48
48
  "kleur": "^4.1.5",
49
49
  "lazyrepo": "0.0.0-alpha.27",
50
- "react": "^19.2.1",
50
+ "react": "^18.3.1",
51
51
  "vitest": "^3.2.4"
52
52
  },
53
53
  "dependencies": {
54
- "@tldraw/state": "4.2.2",
55
- "@tldraw/store": "4.2.2",
56
- "@tldraw/utils": "4.2.2",
57
- "@tldraw/validate": "4.2.2"
54
+ "@tldraw/state": "4.2.3",
55
+ "@tldraw/store": "4.2.3",
56
+ "@tldraw/utils": "4.2.3",
57
+ "@tldraw/validate": "4.2.3"
58
58
  },
59
59
  "peerDependencies": {
60
- "react": "^18.2.0 || ^19.2.1",
61
- "react-dom": "^18.2.0 || ^19.2.1"
60
+ "react": "^18.2.0 || ^19.0.0",
61
+ "react-dom": "^18.2.0 || ^19.0.0"
62
62
  },
63
63
  "module": "dist-esm/index.mjs",
64
64
  "source": "src/index.ts",
@@ -1,4 +1,4 @@
1
- import { devFreeze, Migration, MigrationId } from '@tldraw/store'
1
+ import { Migration, MigrationId } from '@tldraw/store'
2
2
  import { mockUniqueId, structuredClone } from '@tldraw/utils'
3
3
  import { vi } from 'vitest'
4
4
  import { createTLSchema } from '../createTLSchema'
@@ -25,14 +25,8 @@ export function getTestMigration(migrationId: MigrationId) {
25
25
  id: migrationId,
26
26
  up: (stuff: any) => {
27
27
  nextNanoId = 0
28
- if (migration.scope === 'record' || migration.scope === 'store') {
29
- const result = structuredClone(stuff)
30
- return migration.up(result) ?? result
31
- }
32
- const storage =
33
- typeof stuff.entries === 'function' ? stuff : new Map(Object.entries(stuff).map(devFreeze))
34
- migration.up(storage)
35
- return typeof stuff.entries === 'function' ? storage : Object.fromEntries(storage.entries())
28
+ const result = structuredClone(stuff)
29
+ return migration.up(result) ?? result
36
30
  },
37
31
  down: (stuff: any) => {
38
32
  nextNanoId = 0
@@ -0,0 +1,96 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getTestMigration } from '../__tests__/migrationTestUtils'
3
+ import { bookmarkAssetVersions } from './TLBookmarkAsset'
4
+
5
+ describe('TLBookmarkAsset', () => {
6
+ describe('MakeUrlsValid migration', () => {
7
+ const { up, down } = getTestMigration(bookmarkAssetVersions.MakeUrlsValid)
8
+
9
+ it('should clean invalid src URLs and preserve valid ones', () => {
10
+ const assetWithInvalidSrc = {
11
+ id: 'asset:bookmark1',
12
+ type: 'bookmark',
13
+ props: {
14
+ title: 'Test Bookmark',
15
+ description: 'Test Description',
16
+ image: 'https://example.com/image.jpg',
17
+ src: 'invalid-url-format',
18
+ },
19
+ }
20
+
21
+ const result = up(assetWithInvalidSrc)
22
+ expect(result.props.src).toBe('')
23
+
24
+ // Test valid URL is preserved
25
+ const assetWithValidSrc = {
26
+ ...assetWithInvalidSrc,
27
+ props: { ...assetWithInvalidSrc.props, src: 'https://example.com' },
28
+ }
29
+ const validResult = up(assetWithValidSrc)
30
+ expect(validResult.props.src).toBe('https://example.com')
31
+ })
32
+
33
+ it('should be a no-op down migration', () => {
34
+ const asset = {
35
+ id: 'asset:bookmark1',
36
+ type: 'bookmark',
37
+ props: {
38
+ title: 'Test Bookmark',
39
+ description: 'Test Description',
40
+ image: 'https://example.com/image.jpg',
41
+ src: 'https://example.com',
42
+ },
43
+ }
44
+
45
+ const result = down(asset)
46
+ expect(result).toEqual(asset)
47
+ })
48
+ })
49
+
50
+ describe('AddFavicon migration', () => {
51
+ const { up, down } = getTestMigration(bookmarkAssetVersions.AddFavicon)
52
+
53
+ it('should add favicon property and clean invalid URLs', () => {
54
+ // Test adding favicon property to asset without one
55
+ const assetWithoutFavicon = {
56
+ id: 'asset:bookmark1',
57
+ type: 'bookmark',
58
+ props: {
59
+ title: 'Test Bookmark',
60
+ description: 'Test Description',
61
+ image: 'https://example.com/image.jpg',
62
+ src: 'https://example.com',
63
+ },
64
+ }
65
+
66
+ const result = up(assetWithoutFavicon)
67
+ expect(result.props.favicon).toBe('')
68
+
69
+ // Test cleaning invalid favicon URL
70
+ const assetWithInvalidFavicon = {
71
+ ...assetWithoutFavicon,
72
+ props: { ...assetWithoutFavicon.props, favicon: 'invalid-url' },
73
+ }
74
+ const cleanResult = up(assetWithInvalidFavicon)
75
+ expect(cleanResult.props.favicon).toBe('')
76
+ })
77
+
78
+ it('should remove favicon property in down migration', () => {
79
+ const assetWithFavicon = {
80
+ id: 'asset:bookmark1',
81
+ type: 'bookmark',
82
+ props: {
83
+ title: 'Test Bookmark',
84
+ description: 'Test Description',
85
+ image: 'https://example.com/image.jpg',
86
+ src: 'https://example.com',
87
+ favicon: 'https://example.com/favicon.ico',
88
+ },
89
+ }
90
+
91
+ const result = down(assetWithFavicon)
92
+ expect(result.props).not.toHaveProperty('favicon')
93
+ expect(result.props.title).toBe('Test Bookmark')
94
+ })
95
+ })
96
+ })
@@ -0,0 +1,213 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getTestMigration } from '../__tests__/migrationTestUtils'
3
+ import { imageAssetVersions } from './TLImageAsset'
4
+
5
+ describe('TLImageAsset', () => {
6
+ describe('AddIsAnimated migration', () => {
7
+ const { up, down } = getTestMigration(imageAssetVersions.AddIsAnimated)
8
+
9
+ it('should add isAnimated property in up migration', () => {
10
+ const assetWithoutIsAnimated = {
11
+ id: 'asset:image1',
12
+ type: 'image',
13
+ props: {
14
+ w: 100,
15
+ h: 100,
16
+ name: 'test.jpg',
17
+ mimeType: 'image/jpeg',
18
+ src: 'https://example.com/test.jpg',
19
+ },
20
+ }
21
+
22
+ const result = up(assetWithoutIsAnimated)
23
+ expect(result.props.isAnimated).toBe(false)
24
+ })
25
+
26
+ it('should remove isAnimated property in down migration', () => {
27
+ const assetWithIsAnimated = {
28
+ id: 'asset:image3',
29
+ type: 'image',
30
+ props: {
31
+ w: 100,
32
+ h: 100,
33
+ name: 'test.jpg',
34
+ mimeType: 'image/jpeg',
35
+ src: 'https://example.com/test.jpg',
36
+ isAnimated: false,
37
+ },
38
+ }
39
+
40
+ const result = down(assetWithIsAnimated)
41
+ expect(result.props).not.toHaveProperty('isAnimated')
42
+ })
43
+ })
44
+
45
+ describe('RenameWidthHeight migration', () => {
46
+ const { up, down } = getTestMigration(imageAssetVersions.RenameWidthHeight)
47
+
48
+ it('should rename width and height to w and h in up migration', () => {
49
+ const assetWithWidthHeight = {
50
+ id: 'asset:image1',
51
+ type: 'image',
52
+ props: {
53
+ width: 800,
54
+ height: 600,
55
+ name: 'test.jpg',
56
+ isAnimated: false,
57
+ mimeType: 'image/jpeg',
58
+ src: 'https://example.com/test.jpg',
59
+ },
60
+ }
61
+
62
+ const result = up(assetWithWidthHeight)
63
+ expect(result.props.w).toBe(800)
64
+ expect(result.props.h).toBe(600)
65
+ expect(result.props).not.toHaveProperty('width')
66
+ expect(result.props).not.toHaveProperty('height')
67
+ })
68
+
69
+ it('should rename w and h to width and height in down migration', () => {
70
+ const assetWithWH = {
71
+ id: 'asset:image3',
72
+ type: 'image',
73
+ props: {
74
+ w: 1024,
75
+ h: 768,
76
+ name: 'test.png',
77
+ isAnimated: false,
78
+ mimeType: 'image/png',
79
+ src: 'https://example.com/test.png',
80
+ },
81
+ }
82
+
83
+ const result = down(assetWithWH)
84
+ expect(result.props.width).toBe(1024)
85
+ expect(result.props.height).toBe(768)
86
+ expect(result.props).not.toHaveProperty('w')
87
+ expect(result.props).not.toHaveProperty('h')
88
+ })
89
+ })
90
+
91
+ describe('MakeUrlsValid migration', () => {
92
+ const { up, down: _down } = getTestMigration(imageAssetVersions.MakeUrlsValid)
93
+
94
+ it('should clean invalid src URLs in up migration', () => {
95
+ const assetWithInvalidSrc = {
96
+ id: 'asset:image1',
97
+ type: 'image',
98
+ props: {
99
+ w: 100,
100
+ h: 100,
101
+ name: 'test.jpg',
102
+ isAnimated: false,
103
+ mimeType: 'image/jpeg',
104
+ src: 'invalid-url-format',
105
+ },
106
+ }
107
+
108
+ const result = up(assetWithInvalidSrc)
109
+ expect(result.props.src).toBe('')
110
+ })
111
+
112
+ it('should preserve valid src URLs in up migration', () => {
113
+ const assetWithValidSrc = {
114
+ id: 'asset:image2',
115
+ type: 'image',
116
+ props: {
117
+ w: 100,
118
+ h: 100,
119
+ name: 'test.jpg',
120
+ isAnimated: false,
121
+ mimeType: 'image/jpeg',
122
+ src: 'https://example.com/test.jpg',
123
+ },
124
+ }
125
+
126
+ const result = up(assetWithValidSrc)
127
+ expect(result.props.src).toBe('https://example.com/test.jpg')
128
+ })
129
+ })
130
+
131
+ describe('AddFileSize migration', () => {
132
+ const { up, down } = getTestMigration(imageAssetVersions.AddFileSize)
133
+
134
+ it('should add fileSize property with -1 default in up migration', () => {
135
+ const assetWithoutFileSize = {
136
+ id: 'asset:image1',
137
+ type: 'image',
138
+ props: {
139
+ w: 100,
140
+ h: 100,
141
+ name: 'test.jpg',
142
+ isAnimated: false,
143
+ mimeType: 'image/jpeg',
144
+ src: 'https://example.com/test.jpg',
145
+ },
146
+ }
147
+
148
+ const result = up(assetWithoutFileSize)
149
+ expect(result.props.fileSize).toBe(-1)
150
+ })
151
+
152
+ it('should remove fileSize property in down migration', () => {
153
+ const assetWithFileSize = {
154
+ id: 'asset:image3',
155
+ type: 'image',
156
+ props: {
157
+ w: 100,
158
+ h: 100,
159
+ name: 'test.jpg',
160
+ isAnimated: false,
161
+ mimeType: 'image/jpeg',
162
+ src: 'https://example.com/test.jpg',
163
+ fileSize: 50000,
164
+ },
165
+ }
166
+
167
+ const result = down(assetWithFileSize)
168
+ expect(result.props).not.toHaveProperty('fileSize')
169
+ })
170
+ })
171
+
172
+ describe('MakeFileSizeOptional migration', () => {
173
+ const { up, down } = getTestMigration(imageAssetVersions.MakeFileSizeOptional)
174
+
175
+ it('should convert fileSize -1 to undefined in up migration', () => {
176
+ const assetWithNegativeFileSize = {
177
+ id: 'asset:image1',
178
+ type: 'image',
179
+ props: {
180
+ w: 100,
181
+ h: 100,
182
+ name: 'test.jpg',
183
+ isAnimated: false,
184
+ mimeType: 'image/jpeg',
185
+ src: 'https://example.com/test.jpg',
186
+ fileSize: -1,
187
+ },
188
+ }
189
+
190
+ const result = up(assetWithNegativeFileSize)
191
+ expect(result.props.fileSize).toBeUndefined()
192
+ })
193
+
194
+ it('should convert undefined fileSize to -1 in down migration', () => {
195
+ const assetWithUndefinedFileSize = {
196
+ id: 'asset:image5',
197
+ type: 'image',
198
+ props: {
199
+ w: 100,
200
+ h: 100,
201
+ name: 'test.jpg',
202
+ isAnimated: false,
203
+ mimeType: 'image/jpeg',
204
+ src: 'https://example.com/test.jpg',
205
+ fileSize: undefined,
206
+ },
207
+ }
208
+
209
+ const result = down(assetWithUndefinedFileSize)
210
+ expect(result.props.fileSize).toBe(-1)
211
+ })
212
+ })
213
+ })
@@ -0,0 +1,105 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getTestMigration } from '../__tests__/migrationTestUtils'
3
+ import { videoAssetVersions } from './TLVideoAsset'
4
+
5
+ describe('TLVideoAsset', () => {
6
+ describe('migrations', () => {
7
+ it('should handle AddIsAnimated migration', () => {
8
+ const { up, down } = getTestMigration(videoAssetVersions.AddIsAnimated)
9
+
10
+ const assetWithoutIsAnimated = {
11
+ id: 'asset:video1',
12
+ type: 'video',
13
+ props: {
14
+ w: 640,
15
+ h: 480,
16
+ name: 'test.mp4',
17
+ mimeType: 'video/mp4',
18
+ src: 'https://example.com/test.mp4',
19
+ },
20
+ }
21
+
22
+ const upResult = up(assetWithoutIsAnimated)
23
+ expect(upResult.props.isAnimated).toBe(false)
24
+
25
+ const downResult = down(upResult)
26
+ expect(downResult.props).not.toHaveProperty('isAnimated')
27
+ })
28
+
29
+ it('should handle RenameWidthHeight migration', () => {
30
+ const { up, down } = getTestMigration(videoAssetVersions.RenameWidthHeight)
31
+
32
+ const assetWithWidthHeight = {
33
+ id: 'asset:video1',
34
+ type: 'video',
35
+ props: {
36
+ width: 1920,
37
+ height: 1080,
38
+ name: 'test.mp4',
39
+ isAnimated: true,
40
+ mimeType: 'video/mp4',
41
+ src: 'https://example.com/test.mp4',
42
+ },
43
+ }
44
+
45
+ const upResult = up(assetWithWidthHeight)
46
+ expect(upResult.props.w).toBe(1920)
47
+ expect(upResult.props.h).toBe(1080)
48
+ expect(upResult.props).not.toHaveProperty('width')
49
+ expect(upResult.props).not.toHaveProperty('height')
50
+
51
+ const downResult = down(upResult)
52
+ expect(downResult.props.width).toBe(1920)
53
+ expect(downResult.props.height).toBe(1080)
54
+ expect(downResult.props).not.toHaveProperty('w')
55
+ expect(downResult.props).not.toHaveProperty('h')
56
+ })
57
+
58
+ it('should handle MakeUrlsValid migration', () => {
59
+ const { up } = getTestMigration(videoAssetVersions.MakeUrlsValid)
60
+
61
+ const assetWithInvalidSrc = {
62
+ id: 'asset:video1',
63
+ type: 'video',
64
+ props: {
65
+ w: 640,
66
+ h: 480,
67
+ name: 'test.mp4',
68
+ isAnimated: true,
69
+ mimeType: 'video/mp4',
70
+ src: 'invalid-url-format',
71
+ },
72
+ }
73
+
74
+ const result = up(assetWithInvalidSrc)
75
+ expect(result.props.src).toBe('')
76
+ })
77
+
78
+ it('should handle MakeFileSizeOptional migration', () => {
79
+ const { up, down } = getTestMigration(videoAssetVersions.MakeFileSizeOptional)
80
+
81
+ const assetWithNegativeFileSize = {
82
+ id: 'asset:video1',
83
+ type: 'video',
84
+ props: {
85
+ w: 640,
86
+ h: 480,
87
+ name: 'test.mp4',
88
+ isAnimated: true,
89
+ mimeType: 'video/mp4',
90
+ src: 'https://example.com/test.mp4',
91
+ fileSize: -1,
92
+ },
93
+ }
94
+
95
+ const upResult = up(assetWithNegativeFileSize)
96
+ expect(upResult.props.fileSize).toBeUndefined()
97
+
98
+ const downResult = down({
99
+ ...assetWithNegativeFileSize,
100
+ props: { ...assetWithNegativeFileSize.props, fileSize: undefined },
101
+ })
102
+ expect(downResult.props.fileSize).toBe(-1)
103
+ })
104
+ })
105
+ })
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getTestMigration } from '../__tests__/migrationTestUtils'
3
+ import { arrowBindingVersions } from './TLArrowBinding'
4
+
5
+ describe('TLArrowBinding', () => {
6
+ describe('AddSnap migration', () => {
7
+ const { up, down } = getTestMigration(arrowBindingVersions.AddSnap)
8
+
9
+ it('should add snap property with default value "none"', () => {
10
+ const oldRecord = {
11
+ props: {
12
+ terminal: 'end',
13
+ normalizedAnchor: { x: 0.5, y: 0.5 },
14
+ isExact: true,
15
+ isPrecise: false,
16
+ },
17
+ }
18
+
19
+ const result = up(oldRecord)
20
+ expect(result.props.snap).toBe('none')
21
+ expect(result.props.terminal).toBe('end')
22
+ })
23
+
24
+ it('should remove snap property on down migration', () => {
25
+ const newRecord = {
26
+ props: {
27
+ terminal: 'end',
28
+ normalizedAnchor: { x: 0.5, y: 0.5 },
29
+ isExact: true,
30
+ isPrecise: false,
31
+ snap: 'center',
32
+ },
33
+ }
34
+
35
+ const result = down(newRecord)
36
+ expect(result.props.snap).toBeUndefined()
37
+ expect(result.props.terminal).toBe('end')
38
+ })
39
+
40
+ it('should be reversible', () => {
41
+ const originalRecord = {
42
+ props: {
43
+ terminal: 'start',
44
+ normalizedAnchor: { x: 0.5, y: 0.5 },
45
+ isExact: true,
46
+ isPrecise: false,
47
+ },
48
+ }
49
+
50
+ const upResult = up(originalRecord)
51
+ const downResult = down(upResult)
52
+ expect(downResult.props).toEqual(originalRecord.props)
53
+ })
54
+ })
55
+ })
@@ -1,3 +1,4 @@
1
+ import { BaseRecord } from '@tldraw/store'
1
2
  import { JsonObject } from '@tldraw/utils'
2
3
  import { T } from '@tldraw/validate'
3
4
  import { idValidator } from '../misc/id-validator'
@@ -9,41 +10,33 @@ import { shapeIdValidator } from '../shapes/TLBaseShape'
9
10
  * Base interface for all binding types in tldraw. Bindings represent relationships
10
11
  * between shapes, such as arrows connecting to other shapes or organizational connections.
11
12
  *
12
- * All default bindings extend this base interface with specific type and property definitions.
13
+ * All bindings extend this base interface with specific type and property definitions.
13
14
  * The binding system enables shapes to maintain relationships that persist through
14
15
  * transformations, movements, and other operations.
15
16
  *
16
- * Custom bindings should be defined by augmenting the TLGlobalBindingPropsMap type and getting the binding type from the TLBinding type.
17
- *
18
17
  * @param Type - String literal type identifying the specific binding type (e.g., 'arrow')
19
18
  * @param Props - Object containing binding-specific properties and configuration
20
19
  *
21
20
  * @example
22
21
  * ```ts
23
- * // Define a default binding type
24
- * interface TLArrowBinding extends TLBaseBinding<'arrow', TLArrowBindingProps> {}
25
- *
26
- * interface TLArrowBindingProps {
27
- * terminal: 'start' | 'end'
28
- * normalizedAnchor: VecModel
29
- * isExact: boolean
30
- * isPrecise: boolean
31
- * snap: ElbowArrowSnap
22
+ * // Define a custom binding type
23
+ * interface MyCustomBinding extends TLBaseBinding<'custom', MyCustomProps> {}
24
+ *
25
+ * interface MyCustomProps {
26
+ * strength: number
27
+ * color: string
32
28
  * }
33
29
  *
34
30
  * // Create a binding instance
35
- * const arrowBinding: TLArrowBinding = {
31
+ * const binding: MyCustomBinding = {
36
32
  * id: 'binding:abc123',
37
33
  * typeName: 'binding',
38
- * type: 'arrow',
34
+ * type: 'custom',
39
35
  * fromId: 'shape:source1',
40
36
  * toId: 'shape:target1',
41
37
  * props: {
42
- * terminal: 'end',
43
- * normalizedAnchor: { x: 0.5, y: 0.5 },
44
- * isExact: false,
45
- * isPrecise: true,
46
- * snap: 'edge'
38
+ * strength: 0.8,
39
+ * color: 'red'
47
40
  * },
48
41
  * meta: {}
49
42
  * }
@@ -51,12 +44,8 @@ import { shapeIdValidator } from '../shapes/TLBaseShape'
51
44
  *
52
45
  * @public
53
46
  */
54
- export interface TLBaseBinding<Type extends string, Props extends object> {
55
- // using real `extends BaseRecord<'binding', TLBindingId>` introduces a circularity in the types
56
- // and for that reason those "base members" have to be declared manually here
57
- readonly id: TLBindingId
58
- readonly typeName: 'binding'
59
-
47
+ export interface TLBaseBinding<Type extends string, Props extends object>
48
+ extends BaseRecord<'binding', TLBindingId> {
60
49
  /** The specific type of this binding (e.g., 'arrow', 'custom') */
61
50
  type: Type
62
51
  /** ID of the source shape in this binding relationship */
@@ -22,9 +22,8 @@ import {
22
22
  getShapePropKeysByStyle,
23
23
  rootShapeMigrations,
24
24
  } from './records/TLShape'
25
- import { RecordProps, TLPropsMigrations, processPropsMigrations } from './recordsWithProps'
25
+ import { TLPropsMigrations, processPropsMigrations } from './recordsWithProps'
26
26
  import { arrowShapeMigrations, arrowShapeProps } from './shapes/TLArrowShape'
27
- import { TLBaseShape } from './shapes/TLBaseShape'
28
27
  import { bookmarkShapeMigrations, bookmarkShapeProps } from './shapes/TLBookmarkShape'
29
28
  import { drawShapeMigrations, drawShapeProps } from './shapes/TLDrawShape'
30
29
  import { embedShapeMigrations, embedShapeProps } from './shapes/TLEmbedShape'
@@ -148,12 +147,7 @@ export const defaultShapeSchemas = {
148
147
  note: { migrations: noteShapeMigrations, props: noteShapeProps },
149
148
  text: { migrations: textShapeMigrations, props: textShapeProps },
150
149
  video: { migrations: videoShapeMigrations, props: videoShapeProps },
151
- } satisfies {
152
- [T in TLDefaultShape['type']]: {
153
- migrations: SchemaPropsInfo['migrations']
154
- props: RecordProps<TLBaseShape<T, Extract<TLDefaultShape, { type: T }>['props']>>
155
- }
156
- }
150
+ } satisfies { [T in TLDefaultShape['type']]: SchemaPropsInfo }
157
151
 
158
152
  /**
159
153
  * Default binding schema configurations for all built-in tldraw binding types.