diff --git a/packages/editor/src/lib/config/TldrawEditorConfig.tsx b/packages/editor/src/lib/config/TldrawEditorConfig.tsx index 8d27f76e3..86bd7f7ef 100644 --- a/packages/editor/src/lib/config/TldrawEditorConfig.tsx +++ b/packages/editor/src/lib/config/TldrawEditorConfig.tsx @@ -1,36 +1,18 @@ import { CLIENT_FIXUP_SCRIPT, - TLAsset, - TLCamera, TLDOCUMENT_ID, - TLDocument, TLInstance, TLInstanceId, - TLInstancePageState, TLInstancePresence, - TLPage, TLRecord, TLShape, TLStore, TLStoreProps, TLUser, - TLUserDocument, TLUserId, - TLUserPresence, - ensureStoreIsUsable, - onValidationFailure, - rootShapeTypeMigrations, - storeMigrations, + createTLSchema, } from '@tldraw/tlschema' -import { - RecordType, - Store, - StoreSchema, - StoreSnapshot, - createRecordType, - defineMigrations, -} from '@tldraw/tlstore' -import { T } from '@tldraw/tlvalidate' +import { RecordType, Store, StoreSchema, StoreSnapshot } from '@tldraw/tlstore' import { Signal } from 'signia' import { TLArrowShapeDef } from '../app/shapeutils/TLArrowUtil/TLArrowUtil' import { TLBookmarkShapeDef } from '../app/shapeutils/TLBookmarkUtil/TLBookmarkUtil' @@ -46,23 +28,6 @@ import { TLTextShapeDef } from '../app/shapeutils/TLTextUtil/TLTextUtil' import { TLVideoShapeDef } from '../app/shapeutils/TLVideoUtil/TLVideoUtil' import { StateNodeConstructor } from '../app/statechart/StateNode' import { TLShapeDef, TLUnknownShapeDef } from './TLShapeDefinition' -import { defaultDerivePresenceState } from './defaultDerivePresenceState' - -const CORE_SHAPE_DEFS = () => - [ - TLDrawShapeDef, - TLTextShapeDef, - TLLineShapeDef, - TLArrowShapeDef, - TLImageShapeDef, - TLVideoShapeDef, - TLGeoShapeDef, - TLNoteShapeDef, - TLGroupShapeDef, - TLBookmarkShapeDef, - TLEmbedShapeDef, - TLFrameShapeDef, - ] as const /** @public */ export class TldrawEditorConfig { @@ -80,68 +45,35 @@ export class TldrawEditorConfig { /** @internal */ derivePresenceState?: (store: TLStore) => Signal }) { - const { - shapes = [], - tools = [], - allowUnknownShapes = false, - derivePresenceState = defaultDerivePresenceState, - } = args + const { shapes = [], tools = [], allowUnknownShapes = false, derivePresenceState } = args this.tools = tools - const allShapeDefs = [...CORE_SHAPE_DEFS(), ...shapes] - this.shapes = allShapeDefs + this.shapes = [ + TLArrowShapeDef, + TLBookmarkShapeDef, + TLDrawShapeDef, + TLEmbedShapeDef, + TLFrameShapeDef, + TLGeoShapeDef, + TLGroupShapeDef, + TLImageShapeDef, + TLLineShapeDef, + TLNoteShapeDef, + TLTextShapeDef, + TLVideoShapeDef, + ...shapes, + ] - const typeSet = new Set() - for (const shapeDef of allShapeDefs) { - if (typeSet.has(shapeDef.type)) { - throw new Error(`Shape type ${shapeDef.type} is already defined`) - } - typeSet.add(shapeDef.type) - } - - const shapeTypeMigrations = defineMigrations({ - currentVersion: rootShapeTypeMigrations.currentVersion, - firstVersion: rootShapeTypeMigrations.firstVersion, - migrators: rootShapeTypeMigrations.migrators, - subTypeKey: 'type', - subTypeMigrations: Object.fromEntries(allShapeDefs.map((def) => [def.type, def.migrations])), + this.storeSchema = createTLSchema({ + allowUnknownShapes, + customShapeDefs: shapes, + derivePresenceState, }) - let shapeValidator = T.union('type', { - ...Object.fromEntries(allShapeDefs.map((def) => [def.type, def.validator])), - }) as T.UnionValidator<'type', any, any> - if (allowUnknownShapes) { - shapeValidator = shapeValidator.validateUnknownVariants((shape) => shape as any) - } - - const shapeRecord = createRecordType('shape', { - migrations: shapeTypeMigrations, - validator: T.model('shape', shapeValidator), - scope: 'document', - }).withDefaultProperties(() => ({ x: 0, y: 0, rotation: 0, isLocked: false })) - this.TLShape = shapeRecord - - this.storeSchema = StoreSchema.create( - { - asset: TLAsset, - camera: TLCamera, - document: TLDocument, - instance: TLInstance, - instance_page_state: TLInstancePageState, - page: TLPage, - shape: shapeRecord, - user: TLUser, - user_document: TLUserDocument, - user_presence: TLUserPresence, - instance_presence: TLInstancePresence, - }, - { - snapshotMigrations: storeMigrations, - onValidationFailure, - ensureStoreIsUsable, - derivePresenceState, - } - ) + this.TLShape = this.storeSchema.types.shape as RecordType< + TLShape, + 'type' | 'props' | 'index' | 'parentId' + > } createStore(config: { diff --git a/packages/tlschema/api-report.md b/packages/tlschema/api-report.md index d5331e62a..0b34c0858 100644 --- a/packages/tlschema/api-report.md +++ b/packages/tlschema/api-report.md @@ -5,13 +5,16 @@ ```ts import { BaseRecord } from '@tldraw/tlstore'; +import { defineMigrations } from '@tldraw/tlstore'; import { ID } from '@tldraw/tlstore'; import { Migrations } from '@tldraw/tlstore'; import { RecordType } from '@tldraw/tlstore'; +import { Signal } from 'signia'; import { Store } from '@tldraw/tlstore'; import { StoreSchema } from '@tldraw/tlstore'; import { StoreSchemaOptions } from '@tldraw/tlstore'; import { StoreSnapshot } from '@tldraw/tlstore'; +import { StoreValidator } from '@tldraw/tlstore'; import { T } from '@tldraw/tlvalidate'; // @internal (undocumented) @@ -102,15 +105,32 @@ export function createShapeValidator( props: Props; }>; +// @public (undocumented) +export function createTLSchema({ customShapeDefs, allowUnknownShapes, derivePresenceState, }: { + customShapeDefs?: readonly CustomShapeTypeInfo[]; + allowUnknownShapes?: boolean; + derivePresenceState?: (store: TLStore) => Signal; +}): StoreSchema; + // @public (undocumented) export const cursorTypeValidator: T.Validator; // @public (undocumented) export const cursorValidator: T.Validator; +// @public (undocumented) +export type CustomShapeTypeInfo = { + type: string; + migrations: ReturnType; + validator?: StoreValidator; +}; + // @internal (undocumented) export const dashValidator: T.Validator<"dashed" | "dotted" | "draw" | "solid">; +// @internal (undocumented) +export const defaultDerivePresenceState: (store: TLStore) => Signal; + // @public (undocumented) export const documentTypeMigrations: Migrations; diff --git a/packages/tlschema/package.json b/packages/tlschema/package.json index 34a45a9c5..22e83fd35 100644 --- a/packages/tlschema/package.json +++ b/packages/tlschema/package.json @@ -63,5 +63,8 @@ "@tldraw/tlvalidate": "workspace:*", "@tldraw/utils": "workspace:*", "nanoid": "^3.0.0" + }, + "peerDependencies": { + "signia": "*" } } diff --git a/packages/tlschema/src/createTLSchema.ts b/packages/tlschema/src/createTLSchema.ts new file mode 100644 index 000000000..7403d261f --- /dev/null +++ b/packages/tlschema/src/createTLSchema.ts @@ -0,0 +1,119 @@ +import { StoreSchema, StoreValidator, createRecordType, defineMigrations } from '@tldraw/tlstore' +import { T } from '@tldraw/tlvalidate' +import { Signal } from 'signia' +import { TLRecord } from './TLRecord' +import { TLStore, TLStoreProps, ensureStoreIsUsable, onValidationFailure } from './TLStore' +import { defaultDerivePresenceState } from './defaultDerivePresenceState' +import { TLAsset } from './records/TLAsset' +import { TLCamera } from './records/TLCamera' +import { TLDocument } from './records/TLDocument' +import { TLInstance } from './records/TLInstance' +import { TLInstancePageState } from './records/TLInstancePageState' +import { TLInstancePresence } from './records/TLInstancePresence' +import { TLPage } from './records/TLPage' +import { TLShape, rootShapeTypeMigrations } from './records/TLShape' +import { TLUser } from './records/TLUser' +import { TLUserDocument } from './records/TLUserDocument' +import { TLUserPresence } from './records/TLUserPresence' +import { storeMigrations } from './schema' +import { arrowShapeMigrations, arrowShapeTypeValidator } from './shapes/TLArrowShape' +import { bookmarkShapeMigrations, bookmarkShapeTypeValidator } from './shapes/TLBookmarkShape' +import { drawShapeMigrations, drawShapeTypeValidator } from './shapes/TLDrawShape' +import { embedShapeMigrations, embedShapeTypeValidator } from './shapes/TLEmbedShape' +import { frameShapeMigrations, frameShapeTypeValidator } from './shapes/TLFrameShape' +import { geoShapeMigrations, geoShapeTypeValidator } from './shapes/TLGeoShape' +import { groupShapeMigrations, groupShapeTypeValidator } from './shapes/TLGroupShape' +import { imageShapeMigrations, imageShapeTypeValidator } from './shapes/TLImageShape' +import { lineShapeMigrations, lineShapeTypeValidator } from './shapes/TLLineShape' +import { noteShapeMigrations, noteShapeTypeValidator } from './shapes/TLNoteShape' +import { textShapeMigrations, textShapeTypeValidator } from './shapes/TLTextShape' +import { videoShapeMigrations, videoShapeTypeValidator } from './shapes/TLVideoShape' + +const CORE_SHAPE_DEFS: readonly CustomShapeTypeInfo[] = [ + { type: 'draw', migrations: drawShapeMigrations, validator: drawShapeTypeValidator }, + { type: 'text', migrations: textShapeMigrations, validator: textShapeTypeValidator }, + { type: 'line', migrations: lineShapeMigrations, validator: lineShapeTypeValidator }, + { type: 'arrow', migrations: arrowShapeMigrations, validator: arrowShapeTypeValidator }, + { type: 'image', migrations: imageShapeMigrations, validator: imageShapeTypeValidator }, + { type: 'video', migrations: videoShapeMigrations, validator: videoShapeTypeValidator }, + { type: 'geo', migrations: geoShapeMigrations, validator: geoShapeTypeValidator }, + { type: 'note', migrations: noteShapeMigrations, validator: noteShapeTypeValidator }, + { type: 'group', migrations: groupShapeMigrations, validator: groupShapeTypeValidator }, + { + type: 'bookmark', + migrations: bookmarkShapeMigrations, + validator: bookmarkShapeTypeValidator, + }, + { type: 'frame', migrations: frameShapeMigrations, validator: frameShapeTypeValidator }, + { type: 'embed', migrations: embedShapeMigrations, validator: embedShapeTypeValidator }, +] + +/** @public */ +export type CustomShapeTypeInfo = { + type: string + migrations: ReturnType + validator?: StoreValidator +} + +/** @public */ +export function createTLSchema({ + customShapeDefs, + allowUnknownShapes, + derivePresenceState, +}: { + customShapeDefs?: readonly CustomShapeTypeInfo[] + allowUnknownShapes?: boolean + derivePresenceState?: (store: TLStore) => Signal +}) { + const allShapeDefs = [...CORE_SHAPE_DEFS, ...(customShapeDefs ?? [])] + const typeSet = new Set() + for (const shapeDef of allShapeDefs) { + if (typeSet.has(shapeDef.type)) { + throw new Error(`Shape type ${shapeDef.type} is already defined`) + } + typeSet.add(shapeDef.type) + } + + const shapeTypeMigrations = defineMigrations({ + currentVersion: rootShapeTypeMigrations.currentVersion, + firstVersion: rootShapeTypeMigrations.firstVersion, + migrators: rootShapeTypeMigrations.migrators, + subTypeKey: 'type', + subTypeMigrations: Object.fromEntries(allShapeDefs.map((def) => [def.type, def.migrations])), + }) + + let shapeValidator = T.union('type', { + ...Object.fromEntries(allShapeDefs.map((def) => [def.type, def.validator ?? (T.any as any)])), + }) as T.UnionValidator<'type', any, any> + if (allowUnknownShapes) { + shapeValidator = shapeValidator.validateUnknownVariants((shape) => shape as any) + } + + const shapeRecord = createRecordType('shape', { + migrations: shapeTypeMigrations, + validator: T.model('shape', shapeValidator), + scope: 'document', + }).withDefaultProperties(() => ({ x: 0, y: 0, rotation: 0, isLocked: false })) + + return StoreSchema.create( + { + asset: TLAsset, + camera: TLCamera, + document: TLDocument, + instance: TLInstance, + instance_page_state: TLInstancePageState, + page: TLPage, + shape: shapeRecord, + user: TLUser, + user_document: TLUserDocument, + user_presence: TLUserPresence, + instance_presence: TLInstancePresence, + }, + { + snapshotMigrations: storeMigrations, + onValidationFailure, + ensureStoreIsUsable, + derivePresenceState: derivePresenceState ?? defaultDerivePresenceState, + } + ) +} diff --git a/packages/editor/src/lib/config/defaultDerivePresenceState.ts b/packages/tlschema/src/defaultDerivePresenceState.ts similarity index 94% rename from packages/editor/src/lib/config/defaultDerivePresenceState.ts rename to packages/tlschema/src/defaultDerivePresenceState.ts index 79ce4a628..74ea52905 100644 --- a/packages/editor/src/lib/config/defaultDerivePresenceState.ts +++ b/packages/tlschema/src/defaultDerivePresenceState.ts @@ -1,5 +1,6 @@ -import { TLInstancePresence, TLStore } from '@tldraw/tlschema' import { Signal, computed } from 'signia' +import { TLStore } from './TLStore' +import { TLInstancePresence } from './records/TLInstancePresence' /** @internal */ export const defaultDerivePresenceState = (store: TLStore): Signal => { diff --git a/packages/tlschema/src/index.ts b/packages/tlschema/src/index.ts index 49724f895..eccd45cef 100644 --- a/packages/tlschema/src/index.ts +++ b/packages/tlschema/src/index.ts @@ -24,6 +24,9 @@ export { type TLVideoAsset, } from './assets/TLVideoAsset' export { createAssetValidator, type TLBaseAsset } from './assets/asset-validation' +export { createTLSchema } from './createTLSchema' +export type { CustomShapeTypeInfo } from './createTLSchema' +export { defaultDerivePresenceState } from './defaultDerivePresenceState' export { CLIENT_FIXUP_SCRIPT, fixupRecord } from './fixup' export { type Box2dModel, type Vec2dModel } from './geometry-types' export { diff --git a/public-yarn.lock b/public-yarn.lock index 679bccbdf..c6f800bce 100644 --- a/public-yarn.lock +++ b/public-yarn.lock @@ -4542,6 +4542,8 @@ __metadata: kleur: ^4.1.5 lazyrepo: 0.0.0-alpha.26 nanoid: ^3.0.0 + peerDependencies: + signia: "*" languageName: unknown linkType: soft