From 731da1bc775204e80f7171af644f692040d12dc8 Mon Sep 17 00:00:00 2001 From: David Sheldrick Date: Thu, 27 Apr 2023 19:03:19 +0100 Subject: [PATCH] derived presence state (#1204) This PR adds - A new `TLInstancePresence` record type, to collect info about the presence state in a particular instance of the editor. This will eventually be used to sync presence data instead of sending instance-only state across the wire. - **Record Scopes** `RecordType` now has a `scope` property which can be one of three things: - `document`: the record belongs to the document and should be synced and persisted freely. Currently: `TLDocument`, `TLPage`, `TLShape`, and `TLAsset` - `instance`: the record belongs to a single instance of the store and should not be synced at all. It should not be persisted directly in most cases, but rather compiled into a kind of 'instance configuration' to store alongside the local document data so that when reopening the associated document it can remember some of the previous instance state. Currently: `TLInstance`, `TLInstancePageState`, `TLCamera`, `TLUser`, `TLUserDocument`, `TLUserPresence` - `presence`: the record belongs to a single instance of the store and should not be persisted, but may be synced using the special presence sync protocol. Currently just `TLInstancePresence` This sets us up for the following changes, which are gonna be pretty high-impact in terms of integrating tldraw into existing systems: - Removing `instanceId` as a config option. Each instance gets a randomly generated ID. - We'd replace it with an `instanceConfig` option that has stuff like selectedIds, camera positions, and so on. Then it's up to library users to get and reinstate the instance config at persistence boundaries. - Removing `userId` as config option, and removing the `TLUser` type altogether. - We might need to revisit when doing auth-enabled features like locking shapes, but I suspect that will be separate. --- lazy.config.ts | 11 +-- package.json | 4 +- packages/assets/package.json | 2 - packages/editor/api-report.md | 5 +- packages/editor/setupTests.js | 2 +- .../src/lib/config/TldrawEditorConfig.tsx | 34 ++++--- .../lib/config/defaultDerivePresenceState.ts | 56 +++++++++++ packages/tldraw/setupTests.js | 2 +- packages/tlschema/api-report.md | 44 ++++++++- packages/tlschema/src/TLRecord.ts | 2 + packages/tlschema/src/index.ts | 1 + packages/tlschema/src/migrations.test.ts | 19 ++++ packages/tlschema/src/records/TLAsset.ts | 1 + packages/tlschema/src/records/TLCamera.ts | 1 + packages/tlschema/src/records/TLDocument.ts | 1 + packages/tlschema/src/records/TLInstance.ts | 1 + .../src/records/TLInstancePageState.ts | 1 + .../src/records/TLInstancePresence.ts | 89 +++++++++++++++++ packages/tlschema/src/records/TLPage.ts | 1 + packages/tlschema/src/records/TLUser.ts | 1 + .../tlschema/src/records/TLUserDocument.ts | 1 + .../tlschema/src/records/TLUserPresence.ts | 1 + packages/tlschema/src/schema.ts | 13 ++- packages/tlstore/api-report.md | 10 +- packages/tlstore/src/lib/RecordType.ts | 18 ++++ packages/tlstore/src/lib/Store.ts | 11 --- packages/tlstore/src/lib/StoreSchema.ts | 8 ++ packages/tlstore/src/lib/compareSchemas.ts | 2 +- .../tlstore/src/lib/test/recordStore.test.ts | 6 +- .../src/lib/test/recordStoreFuzzing.test.ts | 2 + .../src/lib/test/recordStoreQueries.test.ts | 2 + .../tlstore/src/lib/test/testSchema.v0.ts | 3 + .../tlstore/src/lib/test/testSchema.v1.ts | 2 + .../tlstore/src/lib/test/validate.test.ts | 2 + packages/ui/setupTests.js | 2 +- packages/utils/api-report.md | 7 ++ packages/utils/src/index.ts | 1 + packages/utils/src/lib/object.ts | 21 ++++ public-yarn.lock | 96 +++++++++---------- scripts/api-check.ts | 3 +- 40 files changed, 396 insertions(+), 93 deletions(-) create mode 100644 packages/editor/src/lib/config/defaultDerivePresenceState.ts create mode 100644 packages/tlschema/src/records/TLInstancePresence.ts diff --git a/lazy.config.ts b/lazy.config.ts index fa0ad3c3a..058d1dae2 100644 --- a/lazy.config.ts +++ b/lazy.config.ts @@ -49,11 +49,10 @@ export function generateSharedTasks(bublic: '' | '/bublic') { cache: { inputs: { include: [ - '{.,./bublic}/packages/*/src/**/*.{ts,tsx}', - '{.,./bublic}/{apps,scripts,e2e}/**/*.{ts,tsx}', - '{.,./bublic}/{apps,packages}/*/tsconfig.json', - '{.,./bublic}/{scripts,e2e}/tsconfig.json', - `${bublic}/config/tsconfig.base.json`, + '{,bublic/}packages/*/src/**/*.{ts,tsx}', + '{,bublic/}{apps,scripts,e2e}/**/*.{ts,tsx}', + '{,bublic/}{apps,packages}/*/tsconfig.json', + '{,bublic/}{scripts,e2e}/tsconfig.json', ], exclude: ['**/dist*/**/*.d.ts'], }, @@ -95,7 +94,7 @@ export function generateSharedTasks(bublic: '' | '/bublic') { baseCommand: `tsx ${bublic}/scripts/api-check.ts`, runsAfter: { 'build:api': {} }, cache: { - inputs: ['**/api/bublic.d.ts'], + inputs: [`${bublic}/packages/*/api/public.d.ts`], }, }, } satisfies LazyConfig['tasks'] diff --git a/package.json b/package.json index e0a44ba46..2a6939a21 100644 --- a/package.json +++ b/package.json @@ -87,8 +87,8 @@ }, "devDependencies": { "@microsoft/api-extractor": "^7.34.1", - "@swc/core": "^1.3.41", - "@swc/jest": "^0.2.24", + "@swc/core": "^1.3.55", + "@swc/jest": "^0.2.26", "@types/glob": "^8.1.0", "auto": "^10.44.0", "fs-extra": "^11.1.0", diff --git a/packages/assets/package.json b/packages/assets/package.json index 581a9879c..7584120dc 100644 --- a/packages/assets/package.json +++ b/packages/assets/package.json @@ -48,8 +48,6 @@ "@tldraw/utils": "workspace:*" }, "devDependencies": { - "@swc/core": "^1.2.204", - "@swc/jest": "^0.2.21", "lazyrepo": "0.0.0-alpha.22", "ts-node-dev": "^1.1.8" }, diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index a55f93ff0..7e28bb40f 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -31,6 +31,7 @@ import { SelectionCorner } from '@tldraw/primitives'; import { SelectionEdge } from '@tldraw/primitives'; import { SelectionHandle } from '@tldraw/primitives'; import { SerializedSchema } from '@tldraw/tlstore'; +import { Signal } from 'signia'; import { StoreSchema } from '@tldraw/tlstore'; import { StoreSnapshot } from '@tldraw/tlstore'; import { StoreValidator } from '@tldraw/tlstore'; @@ -62,6 +63,7 @@ import { TLImageShape } from '@tldraw/tlschema'; import { TLInstance } from '@tldraw/tlschema'; import { TLInstanceId } from '@tldraw/tlschema'; import { TLInstancePageState } from '@tldraw/tlschema'; +import { TLInstancePresence } from '@tldraw/tlschema'; import { TLInstancePropsForNextShape } from '@tldraw/tlschema'; import { TLLineShape } from '@tldraw/tlschema'; import { TLNoteShape } from '@tldraw/tlschema'; @@ -1797,10 +1799,11 @@ export function TldrawEditor(props: TldrawEditorProps): JSX.Element; // @public (undocumented) export class TldrawEditorConfig { - constructor({ shapes, tools, allowUnknownShapes, }: { + constructor(args: { shapes?: readonly TLShapeDef[]; tools?: readonly StateNodeConstructor[]; allowUnknownShapes?: boolean; + derivePresenceState?: (store: TLStore) => Signal; }); // (undocumented) createStore(config: { diff --git a/packages/editor/setupTests.js b/packages/editor/setupTests.js index 23a09c1c2..b61be23d3 100644 --- a/packages/editor/setupTests.js +++ b/packages/editor/setupTests.js @@ -1,6 +1,6 @@ require('fake-indexeddb/auto') global.ResizeObserver = require('resize-observer-polyfill') -global.crypto = new (require('@peculiar/webcrypto').Crypto)() +global.crypto ??= new (require('@peculiar/webcrypto').Crypto)() global.FontFace = class FontFace { load() { return Promise.resolve() diff --git a/packages/editor/src/lib/config/TldrawEditorConfig.tsx b/packages/editor/src/lib/config/TldrawEditorConfig.tsx index a1b2da5f7..8d27f76e3 100644 --- a/packages/editor/src/lib/config/TldrawEditorConfig.tsx +++ b/packages/editor/src/lib/config/TldrawEditorConfig.tsx @@ -1,16 +1,13 @@ import { CLIENT_FIXUP_SCRIPT, - ensureStoreIsUsable, - onValidationFailure, - rootShapeTypeMigrations, - storeMigrations, TLAsset, TLCamera, - TLDocument, TLDOCUMENT_ID, + TLDocument, TLInstance, TLInstanceId, TLInstancePageState, + TLInstancePresence, TLPage, TLRecord, TLShape, @@ -20,16 +17,21 @@ import { TLUserDocument, TLUserId, TLUserPresence, + ensureStoreIsUsable, + onValidationFailure, + rootShapeTypeMigrations, + storeMigrations, } from '@tldraw/tlschema' import { - createRecordType, - defineMigrations, RecordType, Store, StoreSchema, StoreSnapshot, + createRecordType, + defineMigrations, } from '@tldraw/tlstore' import { T } from '@tldraw/tlvalidate' +import { Signal } from 'signia' import { TLArrowShapeDef } from '../app/shapeutils/TLArrowUtil/TLArrowUtil' import { TLBookmarkShapeDef } from '../app/shapeutils/TLBookmarkUtil/TLBookmarkUtil' import { TLDrawShapeDef } from '../app/shapeutils/TLDrawUtil/TLDrawUtil' @@ -44,6 +46,7 @@ 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 = () => [ @@ -70,15 +73,19 @@ export class TldrawEditorConfig { readonly TLShape: RecordType readonly tools: readonly StateNodeConstructor[] - constructor({ - shapes = [], - tools = [], - allowUnknownShapes = false, - }: { + constructor(args: { shapes?: readonly TLShapeDef[] tools?: readonly StateNodeConstructor[] allowUnknownShapes?: boolean + /** @internal */ + derivePresenceState?: (store: TLStore) => Signal }) { + const { + shapes = [], + tools = [], + allowUnknownShapes = false, + derivePresenceState = defaultDerivePresenceState, + } = args this.tools = tools const allShapeDefs = [...CORE_SHAPE_DEFS(), ...shapes] @@ -110,6 +117,7 @@ export class TldrawEditorConfig { 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 @@ -125,11 +133,13 @@ export class TldrawEditorConfig { user: TLUser, user_document: TLUserDocument, user_presence: TLUserPresence, + instance_presence: TLInstancePresence, }, { snapshotMigrations: storeMigrations, onValidationFailure, ensureStoreIsUsable, + derivePresenceState, } ) } diff --git a/packages/editor/src/lib/config/defaultDerivePresenceState.ts b/packages/editor/src/lib/config/defaultDerivePresenceState.ts new file mode 100644 index 000000000..79ce4a628 --- /dev/null +++ b/packages/editor/src/lib/config/defaultDerivePresenceState.ts @@ -0,0 +1,56 @@ +import { TLInstancePresence, TLStore } from '@tldraw/tlschema' +import { Signal, computed } from 'signia' + +/** @internal */ +export const defaultDerivePresenceState = (store: TLStore): Signal => { + const $instance = store.query.record('instance', () => ({ + id: { eq: store.props.instanceId }, + })) + const $user = store.query.record('user', () => ({ id: { eq: store.props.userId } })) + const $userPresence = store.query.record('user_presence', () => ({ + userId: { eq: store.props.userId }, + })) + const $pageState = store.query.record('instance_page_state', () => ({ + instanceId: { eq: store.props.instanceId }, + pageId: { eq: $instance.value?.currentPageId ?? ('' as any) }, + })) + const $camera = store.query.record('camera', () => ({ + id: { eq: $pageState.value?.cameraId ?? ('' as any) }, + })) + return computed('instancePresence', () => { + const pageState = $pageState.value + const instance = $instance.value + const user = $user.value + const userPresence = $userPresence.value + const camera = $camera.value + if (!pageState || !instance || !user || !userPresence || !camera) { + return null + } + + return TLInstancePresence.create({ + id: TLInstancePresence.createCustomId(store.props.instanceId), + instanceId: store.props.instanceId, + selectedIds: pageState.selectedIds, + brush: instance.brush, + scribble: instance.scribble, + userId: store.props.userId, + userName: user.name, + followingUserId: instance.followingUserId, + camera: { + x: camera.x, + y: camera.y, + z: camera.z, + }, + color: userPresence.color, + currentPageId: instance.currentPageId, + cursor: { + x: userPresence.cursor.x, + y: userPresence.cursor.y, + rotation: instance.cursor.rotation, + type: instance.cursor.type, + }, + lastActivityTimestamp: userPresence.lastActivityTimestamp, + screenBounds: instance.screenBounds, + }) + }) +} diff --git a/packages/tldraw/setupTests.js b/packages/tldraw/setupTests.js index a23006c43..3d6e91eb9 100644 --- a/packages/tldraw/setupTests.js +++ b/packages/tldraw/setupTests.js @@ -1,7 +1,7 @@ require('fake-indexeddb/auto') require('jest-canvas-mock') global.ResizeObserver = require('resize-observer-polyfill') -global.crypto = new (require('@peculiar/webcrypto').Crypto)() +global.crypto ??= new (require('@peculiar/webcrypto').Crypto)() global.FontFace = class FontFace { load() { return Promise.resolve() diff --git a/packages/tlschema/api-report.md b/packages/tlschema/api-report.md index 294e60ccb..30b80fd17 100644 --- a/packages/tlschema/api-report.md +++ b/packages/tlschema/api-report.md @@ -1011,6 +1011,48 @@ export const TLInstancePageState: RecordType; +// @public (undocumented) +export interface TLInstancePresence extends BaseRecord<'instance_presence'> { + // (undocumented) + brush: Box2dModel | null; + // (undocumented) + camera: { + x: number; + y: number; + z: number; + }; + // (undocumented) + color: string; + // (undocumented) + currentPageId: TLPageId; + // (undocumented) + cursor: { + x: number; + y: number; + type: TLCursor['type']; + rotation: number; + }; + // (undocumented) + followingUserId: null | TLUserId; + // (undocumented) + instanceId: TLInstanceId; + // (undocumented) + lastActivityTimestamp: number; + // (undocumented) + screenBounds: Box2dModel; + // (undocumented) + scribble: null | TLScribble; + // (undocumented) + selectedIds: TLShapeId[]; + // (undocumented) + userId: TLUserId; + // (undocumented) + userName: string; +} + +// @public (undocumented) +export const TLInstancePresence: RecordType; + // @public (undocumented) export type TLInstancePropsForNextShape = Pick; @@ -1078,7 +1120,7 @@ export type TLPageId = ID; export type TLParentId = TLPageId | TLShapeId; // @public (undocumented) -export type TLRecord = TLAsset | TLCamera | TLDocument | TLInstance | TLInstancePageState | TLPage | TLShape | TLUser | TLUserDocument | TLUserPresence; +export type TLRecord = TLAsset | TLCamera | TLDocument | TLInstance | TLInstancePageState | TLInstancePresence | TLPage | TLShape | TLUser | TLUserDocument | TLUserPresence; // @public (undocumented) export type TLScribble = { diff --git a/packages/tlschema/src/TLRecord.ts b/packages/tlschema/src/TLRecord.ts index 5f0886941..a75a10c52 100644 --- a/packages/tlschema/src/TLRecord.ts +++ b/packages/tlschema/src/TLRecord.ts @@ -3,6 +3,7 @@ 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 } from './records/TLShape' import { TLUser } from './records/TLUser' @@ -21,3 +22,4 @@ export type TLRecord = | TLUser | TLUserDocument | TLUserPresence + | TLInstancePresence diff --git a/packages/tlschema/src/index.ts b/packages/tlschema/src/index.ts index b23f9e3d4..49724f895 100644 --- a/packages/tlschema/src/index.ts +++ b/packages/tlschema/src/index.ts @@ -59,6 +59,7 @@ export { instancePageStateTypeValidator, type TLInstancePageStateId, } from './records/TLInstancePageState' +export { TLInstancePresence } from './records/TLInstancePresence' export { TLPage, pageTypeMigrations, pageTypeValidator, type TLPageId } from './records/TLPage' export { createCustomShapeId, diff --git a/packages/tlschema/src/migrations.test.ts b/packages/tlschema/src/migrations.test.ts index 474f232e7..173c1dfe1 100644 --- a/packages/tlschema/src/migrations.test.ts +++ b/packages/tlschema/src/migrations.test.ts @@ -172,6 +172,7 @@ describe('TLImageAsset AddIsAnimated', () => { const ShapeRecord = createRecordType('shape', { validator: { validate: (record) => record as TLShape }, + scope: 'document', }) describe('Store removing Icon and Code shapes', () => { @@ -634,6 +635,24 @@ describe('Add crop=null to image shapes', () => { }) }) +describe('Adding instance_presence to the schema', () => { + const { up, down } = storeMigrations.migrators[2] + + test('up works as expected', () => { + expect(up({})).toEqual({}) + }) + test('down works as expected', () => { + expect( + down({ + 'instance_presence:123': { id: 'instance_presence:123', typeName: 'instance_presence' }, + 'instance:123': { id: 'instance:123', typeName: 'instance' }, + }) + ).toEqual({ + 'instance:123': { id: 'instance:123', typeName: 'instance' }, + }) + }) +}) + /* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */ for (const migrator of allMigrators) { diff --git a/packages/tlschema/src/records/TLAsset.ts b/packages/tlschema/src/records/TLAsset.ts index c177254d6..6049ea99b 100644 --- a/packages/tlschema/src/records/TLAsset.ts +++ b/packages/tlschema/src/records/TLAsset.ts @@ -60,6 +60,7 @@ export type TLAssetPartial = T extends T export const TLAsset = createRecordType('asset', { migrations: assetTypeMigrations, validator: assetTypeValidator, + scope: 'document', }) /** @public */ diff --git a/packages/tlschema/src/records/TLCamera.ts b/packages/tlschema/src/records/TLCamera.ts index b5f4e81dc..41e71e3d9 100644 --- a/packages/tlschema/src/records/TLCamera.ts +++ b/packages/tlschema/src/records/TLCamera.ts @@ -49,6 +49,7 @@ export const cameraTypeMigrations = defineMigrations({ export const TLCamera = createRecordType('camera', { migrations: cameraTypeMigrations, validator: cameraTypeValidator, + scope: 'instance', }).withDefaultProperties( (): Omit => ({ x: 0, diff --git a/packages/tlschema/src/records/TLDocument.ts b/packages/tlschema/src/records/TLDocument.ts index e0c780158..9ff7773c3 100644 --- a/packages/tlschema/src/records/TLDocument.ts +++ b/packages/tlschema/src/records/TLDocument.ts @@ -41,6 +41,7 @@ export const documentTypeMigrations = defineMigrations({ export const TLDocument = createRecordType('document', { migrations: documentTypeMigrations, validator: documentTypeValidator, + scope: 'document', }).withDefaultProperties( (): Omit => ({ gridSize: 10, diff --git a/packages/tlschema/src/records/TLInstance.ts b/packages/tlschema/src/records/TLInstance.ts index d8988a75a..2eabb8038 100644 --- a/packages/tlschema/src/records/TLInstance.ts +++ b/packages/tlschema/src/records/TLInstance.ts @@ -213,6 +213,7 @@ export const instanceTypeMigrations = defineMigrations({ export const TLInstance = createRecordType('instance', { migrations: instanceTypeMigrations, validator: instanceTypeValidator, + scope: 'instance', }).withDefaultProperties( (): Omit => ({ followingUserId: null, diff --git a/packages/tlschema/src/records/TLInstancePageState.ts b/packages/tlschema/src/records/TLInstancePageState.ts index ea70bf001..ee8579bf3 100644 --- a/packages/tlschema/src/records/TLInstancePageState.ts +++ b/packages/tlschema/src/records/TLInstancePageState.ts @@ -74,6 +74,7 @@ export const instancePageStateMigrations = defineMigrations({ export const TLInstancePageState = createRecordType('instance_page_state', { migrations: instancePageStateMigrations, validator: instancePageStateTypeValidator, + scope: 'instance', }).withDefaultProperties( (): Omit< TLInstancePageState, diff --git a/packages/tlschema/src/records/TLInstancePresence.ts b/packages/tlschema/src/records/TLInstancePresence.ts new file mode 100644 index 000000000..81bd95b28 --- /dev/null +++ b/packages/tlschema/src/records/TLInstancePresence.ts @@ -0,0 +1,89 @@ +import { BaseRecord, createRecordType, defineMigrations, ID } from '@tldraw/tlstore' +import { T } from '@tldraw/tlvalidate' +import { Box2dModel } from '../geometry-types' +import { cursorTypeValidator, scribbleTypeValidator, TLCursor, TLScribble } from '../ui-types' +import { idValidator, userIdValidator } from '../validation' +import { TLInstanceId } from './TLInstance' +import { TLPageId } from './TLPage' +import { TLShapeId } from './TLShape' +import { TLUserId } from './TLUser' + +/** @public */ +export interface TLInstancePresence extends BaseRecord<'instance_presence'> { + instanceId: TLInstanceId + userId: TLUserId + userName: string + lastActivityTimestamp: number + color: string // can be any hex color + camera: { x: number; y: number; z: number } + selectedIds: TLShapeId[] + currentPageId: TLPageId + brush: Box2dModel | null + scribble: TLScribble | null + screenBounds: Box2dModel + followingUserId: TLUserId | null + cursor: { + x: number + y: number + type: TLCursor['type'] + rotation: number + } +} + +/** @public */ +export type TLInstancePresenceID = ID + +// --- VALIDATION --- +/** @public */ +export const instancePresenceTypeValidator: T.Validator = T.model( + 'instance_presence', + T.object({ + instanceId: idValidator('instance'), + typeName: T.literal('instance_presence'), + id: idValidator('instance_presence'), + userId: userIdValidator, + userName: T.string, + lastActivityTimestamp: T.number, + followingUserId: userIdValidator.nullable(), + cursor: T.object({ + x: T.number, + y: T.number, + type: cursorTypeValidator, + rotation: T.number, + }), + color: T.string, + camera: T.object({ + x: T.number, + y: T.number, + z: T.number, + }), + screenBounds: T.boxModel, + selectedIds: T.arrayOf(idValidator('shape')), + currentPageId: idValidator('page'), + brush: T.boxModel.nullable(), + scribble: scribbleTypeValidator.nullable(), + }) +) + +// --- MIGRATIONS --- +// STEP 1: Add a new version number here, give it a meaningful name. +// It should be 1 higher than the current version +const Versions = { + Initial: 0, +} as const + +export const userPresenceTypeMigrations = defineMigrations({ + // STEP 2: Update the current version to point to your latest version + currentVersion: Versions.Initial, + firstVersion: Versions.Initial, + migrators: { + // STEP 3: Add an up+down migration for the new version here + }, +}) + +/** @public */ +export const TLInstancePresence = createRecordType('instance_presence', { + migrations: userPresenceTypeMigrations, + validator: instancePresenceTypeValidator, + scope: 'presence', +}) diff --git a/packages/tlschema/src/records/TLPage.ts b/packages/tlschema/src/records/TLPage.ts index bf848c9b4..b7dfdfcfb 100644 --- a/packages/tlschema/src/records/TLPage.ts +++ b/packages/tlschema/src/records/TLPage.ts @@ -47,4 +47,5 @@ export const pageTypeMigrations = defineMigrations({ export const TLPage = createRecordType('page', { migrations: pageTypeMigrations, validator: pageTypeValidator, + scope: 'document', }) diff --git a/packages/tlschema/src/records/TLUser.ts b/packages/tlschema/src/records/TLUser.ts index 353d6a5dc..816303b47 100644 --- a/packages/tlschema/src/records/TLUser.ts +++ b/packages/tlschema/src/records/TLUser.ts @@ -48,6 +48,7 @@ export const userTypeMigrations = defineMigrations({ export const TLUser = createRecordType('user', { migrations: userTypeMigrations, validator: userTypeValidator, + scope: 'instance', }).withDefaultProperties((): Omit => { let lang if (typeof window !== 'undefined' && window.navigator) { diff --git a/packages/tlschema/src/records/TLUserDocument.ts b/packages/tlschema/src/records/TLUserDocument.ts index 6a69ebb43..b6f767b00 100644 --- a/packages/tlschema/src/records/TLUserDocument.ts +++ b/packages/tlschema/src/records/TLUserDocument.ts @@ -87,6 +87,7 @@ export const userDocumentTypeMigrations = defineMigrations({ export const TLUserDocument = createRecordType('user_document', { migrations: userDocumentTypeMigrations, validator: userDocumentTypeValidator, + scope: 'instance', }).withDefaultProperties( (): Omit => ({ /* STEP 6: Add any new default values for properties here */ diff --git a/packages/tlschema/src/records/TLUserPresence.ts b/packages/tlschema/src/records/TLUserPresence.ts index 21eae25b9..78dc716b8 100644 --- a/packages/tlschema/src/records/TLUserPresence.ts +++ b/packages/tlschema/src/records/TLUserPresence.ts @@ -65,6 +65,7 @@ export const userPresenceTypeMigrations = defineMigrations({ export const TLUserPresence = createRecordType('user_presence', { migrations: userPresenceTypeMigrations, validator: userPresenceTypeValidator, + scope: 'instance', }).withDefaultProperties( (): Omit => ({ lastUsedInstanceId: null, diff --git a/packages/tlschema/src/schema.ts b/packages/tlschema/src/schema.ts index e95944fb5..8175eb914 100644 --- a/packages/tlschema/src/schema.ts +++ b/packages/tlschema/src/schema.ts @@ -7,13 +7,14 @@ import { TLRecord } from './TLRecord' const Versions = { Initial: 0, RemoveCodeAndIconShapeTypes: 1, + AddInstancePresenceType: 2, } as const /** @public */ export const storeMigrations = defineMigrations({ // STEP 2: Update the current version to point to your latest version firstVersion: Versions.Initial, - currentVersion: Versions.RemoveCodeAndIconShapeTypes, + currentVersion: Versions.AddInstancePresenceType, migrators: { // STEP 3: Add an up+down migration for the new version here [Versions.RemoveCodeAndIconShapeTypes]: { @@ -29,5 +30,15 @@ export const storeMigrations = defineMigrations({ return store }, }, + [Versions.AddInstancePresenceType]: { + up: (store: StoreSnapshot) => { + return store + }, + down: (store: StoreSnapshot) => { + return Object.fromEntries( + Object.entries(store).filter(([_, v]) => v.typeName !== 'instance_presence') + ) + }, + }, }, }) diff --git a/packages/tlstore/api-report.md b/packages/tlstore/api-report.md index becf20e81..d6a4086f1 100644 --- a/packages/tlstore/api-report.md +++ b/packages/tlstore/api-report.md @@ -6,6 +6,7 @@ import { Atom } from 'signia'; import { Computed } from 'signia'; +import { Signal } from 'signia'; // @public export type AllRecords> = ExtractR>; @@ -31,7 +32,7 @@ export type CollectionDiff = { export function compareRecordVersions(a: RecordVersion, b: RecordVersion): -1 | 0 | 1; // @public (undocumented) -export const compareSchemas: (a: SerializedSchema, b: SerializedSchema) => number; +export const compareSchemas: (a: SerializedSchema, b: SerializedSchema) => -1 | 0 | 1; // @public export type ComputedCache = { @@ -42,6 +43,7 @@ export type ComputedCache = { export function createRecordType(typeName: R['typeName'], config: { migrations?: Migrations; validator: StoreValidator; + scope: Scope; }): RecordType>; // @public (undocumented) @@ -158,6 +160,7 @@ export class RecordType R; } | StoreValidator; + readonly scope?: Scope; }); clone(record: R): R; create(properties: Pick & Omit, RequiredProperties>): R; @@ -170,6 +173,8 @@ export class RecordType; + // (undocumented) + readonly scope: Scope; readonly typeName: R['typeName']; validate(record: unknown): R; // (undocumented) @@ -274,6 +279,8 @@ export class StoreSchema { // (undocumented) get currentStoreVersion(): number; // @internal (undocumented) + derivePresenceState(store: Store): Signal | undefined; + // @internal (undocumented) ensureStoreIsUsable(store: Store): void; // (undocumented) migratePersistedRecord(record: R, persistedSchema: SerializedSchema, direction?: 'down' | 'up'): MigrationResult; @@ -302,6 +309,7 @@ export type StoreSchemaOptions = { recordBefore: null | R; }) => R; ensureStoreIsUsable?: (store: Store) => void; + derivePresenceState?: (store: Store) => Signal; }; // @public diff --git a/packages/tlstore/src/lib/RecordType.ts b/packages/tlstore/src/lib/RecordType.ts index 5676acf77..b9512b648 100644 --- a/packages/tlstore/src/lib/RecordType.ts +++ b/packages/tlstore/src/lib/RecordType.ts @@ -6,6 +6,17 @@ import { Migrations } from './migrate' export type RecordTypeRecord> = ReturnType +/** + * Defines the scope of the record + * + * instance: The record belongs to a single instance of the store. It should not be synced, and any persistence logic should 'de-instance-ize' the record before persisting it, and apply the reverse when rehydrating. + * document: The record is persisted and synced. It is available to all store instances. + * presence: The record belongs to a single instance of the store. It may be synced to other instances, but other instances should not make changes to it. It should not be persisted. + * + * @public + * */ +export type Scope = 'instance' | 'document' | 'presence' + /** * A record type is a type that can be stored in a record store. It is created with * `createRecordType`. @@ -20,6 +31,8 @@ export class RecordType< readonly migrations: Migrations readonly validator: StoreValidator | { validate: (r: unknown) => R } + readonly scope: Scope + constructor( /** * The unique type associated with this record. @@ -32,11 +45,13 @@ export class RecordType< readonly createDefaultProperties: () => Exclude, RequiredProperties> readonly migrations: Migrations readonly validator?: StoreValidator | { validate: (r: unknown) => R } + readonly scope?: Scope } ) { this.createDefaultProperties = config.createDefaultProperties this.migrations = config.migrations this.validator = config.validator ?? { validate: (r: unknown) => r as R } + this.scope = config.scope ?? 'document' } /** @@ -174,6 +189,7 @@ export class RecordType< createDefaultProperties: createDefaultProperties as any, migrations: this.migrations, validator: this.validator, + scope: this.scope, }) } @@ -204,12 +220,14 @@ export function createRecordType( migrations?: Migrations // todo: optional validations validator: StoreValidator + scope: Scope } ): RecordType> { return new RecordType>(typeName, { createDefaultProperties: () => ({} as any), migrations: config.migrations ?? { currentVersion: 0, firstVersion: 0, migrators: {} }, validator: config.validator, + scope: config.scope, }) } diff --git a/packages/tlstore/src/lib/Store.ts b/packages/tlstore/src/lib/Store.ts index 9a537b304..ebdd39cb0 100644 --- a/packages/tlstore/src/lib/Store.ts +++ b/packages/tlstore/src/lib/Store.ts @@ -766,15 +766,4 @@ class HistoryAccumulator { hasChanges() { return this._history.length > 0 } - - /** - * Ensure that the store is usable. A class that extends this store should override this method. - * - * @param config - The configuration object. This can be any object that allows the store to - * validate that it is usable; the extending class should specify the type. - * @public - */ - ensureStoreIsUsable(_config = {} as any): void { - return - } } diff --git a/packages/tlstore/src/lib/StoreSchema.ts b/packages/tlstore/src/lib/StoreSchema.ts index 2ff83420f..e6c2b642a 100644 --- a/packages/tlstore/src/lib/StoreSchema.ts +++ b/packages/tlstore/src/lib/StoreSchema.ts @@ -1,4 +1,5 @@ import { getOwnProperty, objectMapValues } from '@tldraw/utils' +import { Signal } from 'signia' import { BaseRecord } from './BaseRecord' import { RecordType } from './RecordType' import { Store, StoreSnapshot } from './Store' @@ -48,6 +49,8 @@ export type StoreSchemaOptions = { }) => R /** @internal */ ensureStoreIsUsable?: (store: Store) => void + /** @internal */ + derivePresenceState?: (store: Store) => Signal } /** @public */ @@ -241,6 +244,11 @@ export class StoreSchema { this.options.ensureStoreIsUsable?.(store) } + /** @internal */ + derivePresenceState(store: Store): Signal | undefined { + return this.options.derivePresenceState?.(store) + } + serialize(): SerializedSchema { return { schemaVersion: 1, diff --git a/packages/tlstore/src/lib/compareSchemas.ts b/packages/tlstore/src/lib/compareSchemas.ts index b4f2839a2..dfb115837 100644 --- a/packages/tlstore/src/lib/compareSchemas.ts +++ b/packages/tlstore/src/lib/compareSchemas.ts @@ -1,7 +1,7 @@ import { SerializedSchema } from './StoreSchema' /** @public */ -export const compareSchemas = (a: SerializedSchema, b: SerializedSchema): number => { +export const compareSchemas = (a: SerializedSchema, b: SerializedSchema): 0 | 1 | -1 => { if (a.schemaVersion > b.schemaVersion) { return 1 } diff --git a/packages/tlstore/src/lib/test/recordStore.test.ts b/packages/tlstore/src/lib/test/recordStore.test.ts index 2d43e6c8f..e043f907e 100644 --- a/packages/tlstore/src/lib/test/recordStore.test.ts +++ b/packages/tlstore/src/lib/test/recordStore.test.ts @@ -10,7 +10,10 @@ interface Book extends BaseRecord<'book'> { numPages: number } -const Book = createRecordType('book', { validator: { validate: (book) => book as Book } }) +const Book = createRecordType('book', { + validator: { validate: (book) => book as Book }, + scope: 'document', +}) interface Author extends BaseRecord<'author'> { name: string @@ -19,6 +22,7 @@ interface Author extends BaseRecord<'author'> { const Author = createRecordType('author', { validator: { validate: (author) => author as Author }, + scope: 'document', }).withDefaultProperties(() => ({ isPseudonym: false, })) diff --git a/packages/tlstore/src/lib/test/recordStoreFuzzing.test.ts b/packages/tlstore/src/lib/test/recordStoreFuzzing.test.ts index e44adcd16..c3da55879 100644 --- a/packages/tlstore/src/lib/test/recordStoreFuzzing.test.ts +++ b/packages/tlstore/src/lib/test/recordStoreFuzzing.test.ts @@ -21,6 +21,7 @@ const Author = createRecordType('author', { return author }, }, + scope: 'document', }).withDefaultProperties(() => ({ age: 23 })) interface Book extends BaseRecord<'book'> { @@ -38,6 +39,7 @@ const Book = createRecordType('book', { return book }, }, + scope: 'document', }) const bookComparator = (a: Book, b: Book) => a.id.localeCompare(b.id) diff --git a/packages/tlstore/src/lib/test/recordStoreQueries.test.ts b/packages/tlstore/src/lib/test/recordStoreQueries.test.ts index fabfd4d1e..c8a0d5edc 100644 --- a/packages/tlstore/src/lib/test/recordStoreQueries.test.ts +++ b/packages/tlstore/src/lib/test/recordStoreQueries.test.ts @@ -19,6 +19,7 @@ const Author = createRecordType('author', { return author }, }, + scope: 'document', }).withDefaultProperties(() => ({ age: 23 })) interface Book extends BaseRecord<'book'> { @@ -36,6 +37,7 @@ const Book = createRecordType('book', { return book }, }, + scope: 'document', }) const authors = { tolkein: Author.create({ name: 'J.R.R. Tolkein' }), diff --git a/packages/tlstore/src/lib/test/testSchema.v0.ts b/packages/tlstore/src/lib/test/testSchema.v0.ts index 1ae2ac501..ad130984c 100644 --- a/packages/tlstore/src/lib/test/testSchema.v0.ts +++ b/packages/tlstore/src/lib/test/testSchema.v0.ts @@ -29,6 +29,7 @@ const User = createRecordType('user', { return record as User }, }, + scope: 'document', }) const ShapeVersion = { @@ -90,6 +91,7 @@ const Shape = createRecordType>('shape', { return record as Shape }, }, + scope: 'document', }) // this interface only exists to be removed @@ -107,6 +109,7 @@ const Org = createRecordType('org', { return record as Org }, }, + scope: 'document', }) export const testSchemaV0 = StoreSchema.create( diff --git a/packages/tlstore/src/lib/test/testSchema.v1.ts b/packages/tlstore/src/lib/test/testSchema.v1.ts index a35cc98bf..024f83c7a 100644 --- a/packages/tlstore/src/lib/test/testSchema.v1.ts +++ b/packages/tlstore/src/lib/test/testSchema.v1.ts @@ -62,6 +62,7 @@ const User = createRecordType('user', { return record as User }, }, + scope: 'document', }).withDefaultProperties(() => ({ /* STEP 6: Add any new default values for properties here */ name: 'New User', @@ -192,6 +193,7 @@ const Shape = createRecordType>('shape', { return record as Shape }, }, + scope: 'document', }).withDefaultProperties(() => ({ x: 0, y: 0, diff --git a/packages/tlstore/src/lib/test/validate.test.ts b/packages/tlstore/src/lib/test/validate.test.ts index fcd275af8..f07cae3e5 100644 --- a/packages/tlstore/src/lib/test/validate.test.ts +++ b/packages/tlstore/src/lib/test/validate.test.ts @@ -21,6 +21,7 @@ const Book = createRecordType('book', { return book }, }, + scope: 'document', }) interface Author extends BaseRecord<'author'> { @@ -39,6 +40,7 @@ const Author = createRecordType('author', { return author }, }, + scope: 'document', }).withDefaultProperties(() => ({ isPseudonym: false, })) diff --git a/packages/ui/setupTests.js b/packages/ui/setupTests.js index f75104137..257b4d80e 100644 --- a/packages/ui/setupTests.js +++ b/packages/ui/setupTests.js @@ -1,3 +1,3 @@ require('fake-indexeddb/auto') global.ResizeObserver = require('resize-observer-polyfill') -global.crypto = new (require('@peculiar/webcrypto').Crypto)() +global.crypto ??= new (require('@peculiar/webcrypto').Crypto)() diff --git a/packages/utils/api-report.md b/packages/utils/api-report.md index 76a4ad09d..a93ce397b 100644 --- a/packages/utils/api-report.md +++ b/packages/utils/api-report.md @@ -37,6 +37,13 @@ export type ErrorResult = { // @internal (undocumented) export function exhaustiveSwitchError(value: never, property?: string): never; +// @internal +export function filterEntries(object: { + [K in Key]: Value; +}, predicate: (key: Key, value: Value) => boolean): { + [K in Key]: Value; +}; + // @internal (undocumented) export function getErrorAnnotations(error: Error): ErrorAnnotations; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 8137e3e2f..503b9c918 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -16,6 +16,7 @@ export { getFirstFromIterable } from './lib/iterable' export { lerp, modulate, rng } from './lib/number' export { deepCopy, + filterEntries, getOwnProperty, hasOwnProperty, objectMapEntries, diff --git a/packages/utils/src/lib/object.ts b/packages/utils/src/lib/object.ts index 9a896df5c..156243976 100644 --- a/packages/utils/src/lib/object.ts +++ b/packages/utils/src/lib/object.ts @@ -87,3 +87,24 @@ export function objectMapEntries(object: { }): Array<[Key, Value]> { return Object.entries(object) as [Key, Value][] } + +/** + * Filters an object using a predicate function. + * @returns a new object with only the entries that pass the predicate + * @internal + */ +export function filterEntries( + object: { [K in Key]: Value }, + predicate: (key: Key, value: Value) => boolean +): { [K in Key]: Value } { + const result: { [K in Key]?: Value } = {} + let didChange = false + for (const [key, value] of objectMapEntries(object)) { + if (predicate(key, value)) { + result[key] = value + } else { + didChange = true + } + } + return didChange ? (result as { [K in Key]: Value }) : object +} diff --git a/public-yarn.lock b/public-yarn.lock index 3b3a76efb..b33bd3631 100644 --- a/public-yarn.lock +++ b/public-yarn.lock @@ -4049,90 +4049,90 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.3.52": - version: 1.3.52 - resolution: "@swc/core-darwin-arm64@npm:1.3.52" +"@swc/core-darwin-arm64@npm:1.3.55": + version: 1.3.55 + resolution: "@swc/core-darwin-arm64@npm:1.3.55" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.3.52": - version: 1.3.52 - resolution: "@swc/core-darwin-x64@npm:1.3.52" +"@swc/core-darwin-x64@npm:1.3.55": + version: 1.3.55 + resolution: "@swc/core-darwin-x64@npm:1.3.55" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.3.52": - version: 1.3.52 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.52" +"@swc/core-linux-arm-gnueabihf@npm:1.3.55": + version: 1.3.55 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.55" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.3.52": - version: 1.3.52 - resolution: "@swc/core-linux-arm64-gnu@npm:1.3.52" +"@swc/core-linux-arm64-gnu@npm:1.3.55": + version: 1.3.55 + resolution: "@swc/core-linux-arm64-gnu@npm:1.3.55" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.3.52": - version: 1.3.52 - resolution: "@swc/core-linux-arm64-musl@npm:1.3.52" +"@swc/core-linux-arm64-musl@npm:1.3.55": + version: 1.3.55 + resolution: "@swc/core-linux-arm64-musl@npm:1.3.55" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.3.52": - version: 1.3.52 - resolution: "@swc/core-linux-x64-gnu@npm:1.3.52" +"@swc/core-linux-x64-gnu@npm:1.3.55": + version: 1.3.55 + resolution: "@swc/core-linux-x64-gnu@npm:1.3.55" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.3.52": - version: 1.3.52 - resolution: "@swc/core-linux-x64-musl@npm:1.3.52" +"@swc/core-linux-x64-musl@npm:1.3.55": + version: 1.3.55 + resolution: "@swc/core-linux-x64-musl@npm:1.3.55" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.3.52": - version: 1.3.52 - resolution: "@swc/core-win32-arm64-msvc@npm:1.3.52" +"@swc/core-win32-arm64-msvc@npm:1.3.55": + version: 1.3.55 + resolution: "@swc/core-win32-arm64-msvc@npm:1.3.55" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.3.52": - version: 1.3.52 - resolution: "@swc/core-win32-ia32-msvc@npm:1.3.52" +"@swc/core-win32-ia32-msvc@npm:1.3.55": + version: 1.3.55 + resolution: "@swc/core-win32-ia32-msvc@npm:1.3.55" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.3.52": - version: 1.3.52 - resolution: "@swc/core-win32-x64-msvc@npm:1.3.52" +"@swc/core-win32-x64-msvc@npm:1.3.55": + version: 1.3.55 + resolution: "@swc/core-win32-x64-msvc@npm:1.3.55" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@swc/core@npm:^1.2.204, @swc/core@npm:^1.3.41": - version: 1.3.52 - resolution: "@swc/core@npm:1.3.52" +"@swc/core@npm:^1.3.55": + version: 1.3.55 + resolution: "@swc/core@npm:1.3.55" dependencies: - "@swc/core-darwin-arm64": 1.3.52 - "@swc/core-darwin-x64": 1.3.52 - "@swc/core-linux-arm-gnueabihf": 1.3.52 - "@swc/core-linux-arm64-gnu": 1.3.52 - "@swc/core-linux-arm64-musl": 1.3.52 - "@swc/core-linux-x64-gnu": 1.3.52 - "@swc/core-linux-x64-musl": 1.3.52 - "@swc/core-win32-arm64-msvc": 1.3.52 - "@swc/core-win32-ia32-msvc": 1.3.52 - "@swc/core-win32-x64-msvc": 1.3.52 + "@swc/core-darwin-arm64": 1.3.55 + "@swc/core-darwin-x64": 1.3.55 + "@swc/core-linux-arm-gnueabihf": 1.3.55 + "@swc/core-linux-arm64-gnu": 1.3.55 + "@swc/core-linux-arm64-musl": 1.3.55 + "@swc/core-linux-x64-gnu": 1.3.55 + "@swc/core-linux-x64-musl": 1.3.55 + "@swc/core-win32-arm64-msvc": 1.3.55 + "@swc/core-win32-ia32-msvc": 1.3.55 + "@swc/core-win32-x64-msvc": 1.3.55 peerDependencies: "@swc/helpers": ^0.5.0 dependenciesMeta: @@ -4159,7 +4159,7 @@ __metadata: peerDependenciesMeta: "@swc/helpers": optional: true - checksum: ae92657347b223ddbcc47d995966517356bbd600f775fcd74805c95eb8b10e80d0db1def315c710675fa40cae3c89cf26c413bccf1ea884066ba435f65425864 + checksum: e8ae32e21e78761597b802bd76bb5f0d819441454c4cc5624c077dfa8cf84760eb589e4a1eb6fdc1b1ec65a4b03f7ab42413952b38234f5c13b8b2afb4d453f4 languageName: node linkType: hard @@ -4172,7 +4172,7 @@ __metadata: languageName: node linkType: hard -"@swc/jest@npm:^0.2.21, @swc/jest@npm:^0.2.24": +"@swc/jest@npm:^0.2.26": version: 0.2.26 resolution: "@swc/jest@npm:0.2.26" dependencies: @@ -4274,8 +4274,6 @@ __metadata: version: 0.0.0-use.local resolution: "@tldraw/assets@workspace:packages/assets" dependencies: - "@swc/core": ^1.2.204 - "@swc/jest": ^0.2.21 "@tldraw/utils": "workspace:*" lazyrepo: 0.0.0-alpha.22 ts-node-dev: ^1.1.8 @@ -4382,8 +4380,8 @@ __metadata: dependencies: "@microsoft/api-extractor": ^7.34.1 "@next/eslint-plugin-next": ^13.3.0 - "@swc/core": ^1.3.41 - "@swc/jest": ^0.2.24 + "@swc/core": ^1.3.55 + "@swc/jest": ^0.2.26 "@types/glob": ^8.1.0 "@types/jest": ^28.1.2 "@types/node": 18.7.3 diff --git a/scripts/api-check.ts b/scripts/api-check.ts index bc10cf61b..a500d5ae2 100755 --- a/scripts/api-check.ts +++ b/scripts/api-check.ts @@ -55,9 +55,8 @@ async function main() { console.log('Checking with tsconfig:', tsconfig) writeFileSync(`${tempDir}/tsconfig.json`, JSON.stringify(tsconfig, null, '\t'), 'utf8') writeFileSync(`${tempDir}/package.json`, JSON.stringify({ dependencies: {} }, null, '\t'), 'utf8') - writeFileSync(`${tempDir}/.yarnrc.yml`, 'nodeLinker: node-modules\n', 'utf8') - await exec('yarn', ['add', ...packagesOurTypesCanDependOn], { pwd: tempDir }) + await exec('npm', ['install', ...packagesOurTypesCanDependOn], { pwd: tempDir }) await exec(resolve('./node_modules/.bin/tsc'), [], { pwd: tempDir }) await exec('rm', ['-rf', tempDir])