{ "version": 3, "sources": ["../../../src/lib/utils/file.ts"], "sourcesContent": ["import {\n\tEditor,\n\tFileHelpers,\n\tMigrationFailureReason,\n\tMigrationResult,\n\tRecordId,\n\tResult,\n\tSerializedSchema,\n\tSerializedStore,\n\tT,\n\tTLAsset,\n\tTLAssetId,\n\tTLRecord,\n\tTLSchema,\n\tTLStore,\n\tUnknownRecord,\n\tcreateTLStore,\n\texhaustiveSwitchError,\n\tpartition,\n\ttransact,\n} from '@tldraw/editor'\nimport { TLUiToastsContextType } from '../ui/hooks/useToastsProvider'\nimport { TLUiTranslationKey } from '../ui/hooks/useTranslation/TLUiTranslationKey'\nimport { buildFromV1Document } from './buildFromV1Document'\n\n/** @public */\nexport const TLDRAW_FILE_MIMETYPE = 'application/vnd.tldraw+json' as const\n\n/** @public */\nexport const TLDRAW_FILE_EXTENSION = '.tldr' as const\n\n// When incrementing this, you'll need to update parseTldrawJsonFile to handle\n// both your new changes and the old file format\nconst LATEST_TLDRAW_FILE_FORMAT_VERSION = 1\n\n/** @public */\nexport interface TldrawFile {\n\ttldrawFileFormatVersion: number\n\tschema: SerializedSchema\n\trecords: UnknownRecord[]\n}\n\nconst tldrawFileValidator: T.Validator = T.object({\n\ttldrawFileFormatVersion: T.nonZeroInteger,\n\tschema: T.object({\n\t\tschemaVersion: T.positiveInteger,\n\t\tstoreVersion: T.positiveInteger,\n\t\trecordVersions: T.dict(\n\t\t\tT.string,\n\t\t\tT.object({\n\t\t\t\tversion: T.positiveInteger,\n\t\t\t\tsubTypeVersions: T.dict(T.string, T.positiveInteger).optional(),\n\t\t\t\tsubTypeKey: T.string.optional(),\n\t\t\t})\n\t\t),\n\t}),\n\trecords: T.arrayOf(\n\t\tT.object({\n\t\t\tid: T.string as T.Validator>,\n\t\t\ttypeName: T.string,\n\t\t}).allowUnknownProperties()\n\t),\n})\n\n/** @public */\nexport function isV1File(data: any) {\n\ttry {\n\t\tif (data.document?.version) {\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t} catch (e) {\n\t\treturn false\n\t}\n}\n\n/** @public */\nexport type TldrawFileParseError =\n\t| { type: 'v1File'; data: any }\n\t| { type: 'notATldrawFile'; cause: unknown }\n\t| { type: 'fileFormatVersionTooNew'; version: number }\n\t| { type: 'migrationFailed'; reason: MigrationFailureReason }\n\t| { type: 'invalidRecords'; cause: unknown }\n\n/** @public */\nexport function parseTldrawJsonFile({\n\tjson,\n\tschema,\n}: {\n\tschema: TLSchema\n\tjson: string\n}): Result {\n\t// first off, we parse .json file and check it matches the general shape of\n\t// a tldraw file\n\tlet data\n\ttry {\n\t\tdata = tldrawFileValidator.validate(JSON.parse(json))\n\t} catch (e) {\n\t\t// could be a v1 file!\n\t\ttry {\n\t\t\tdata = JSON.parse(json)\n\t\t\tif (isV1File(data)) {\n\t\t\t\treturn Result.err({ type: 'v1File', data })\n\t\t\t}\n\t\t} catch (e) {\n\t\t\t// noop\n\t\t}\n\n\t\treturn Result.err({ type: 'notATldrawFile', cause: e })\n\t}\n\n\t// if the file format version isn't supported, we can't open it - it's\n\t// probably from a newer version of tldraw\n\tif (data.tldrawFileFormatVersion > LATEST_TLDRAW_FILE_FORMAT_VERSION) {\n\t\treturn Result.err({\n\t\t\ttype: 'fileFormatVersionTooNew',\n\t\t\tversion: data.tldrawFileFormatVersion,\n\t\t})\n\t}\n\n\t// even if the file version is up to date, it might contain old-format\n\t// records. lets create a store with the records and migrate it to the\n\t// latest version\n\tlet migrationResult: MigrationResult>\n\ttry {\n\t\tconst storeSnapshot = Object.fromEntries(data.records.map((r) => [r.id, r as TLRecord]))\n\t\tmigrationResult = schema.migrateStoreSnapshot({ store: storeSnapshot, schema: data.schema })\n\t} catch (e) {\n\t\t// junk data in the migration\n\t\treturn Result.err({ type: 'invalidRecords', cause: e })\n\t}\n\t// if the migration failed, we can't open the file\n\tif (migrationResult.type === 'error') {\n\t\treturn Result.err({ type: 'migrationFailed', reason: migrationResult.reason })\n\t}\n\n\t// at this stage, the store should have records at the latest versions, so\n\t// we should be able to validate them. if any of the records at this stage\n\t// are invalid, we don't open the file\n\ttry {\n\t\treturn Result.ok(\n\t\t\tcreateTLStore({\n\t\t\t\tinitialData: migrationResult.value,\n\t\t\t\tschema,\n\t\t\t})\n\t\t)\n\t} catch (e) {\n\t\t// junk data in the records (they're not validated yet!) could cause the\n\t\t// migrations to crash. We treat any throw from a migration as an\n\t\t// invalid record\n\t\treturn Result.err({ type: 'invalidRecords', cause: e })\n\t}\n}\n\n/** @public */\nexport async function serializeTldrawJson(store: TLStore): Promise {\n\tconst records: TLRecord[] = []\n\tconst usedAssets = new Set()\n\tconst assets: TLAsset[] = []\n\tfor (const record of store.allRecords()) {\n\t\tswitch (record.typeName) {\n\t\t\tcase 'asset':\n\t\t\t\tif (\n\t\t\t\t\trecord.type !== 'bookmark' &&\n\t\t\t\t\trecord.props.src &&\n\t\t\t\t\t!record.props.src.startsWith('data:')\n\t\t\t\t) {\n\t\t\t\t\tlet assetSrcToSave\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// try to save the asset as a base64 string\n\t\t\t\t\t\tassetSrcToSave = await FileHelpers.fileToBase64(\n\t\t\t\t\t\t\tawait (await fetch(record.props.src)).blob()\n\t\t\t\t\t\t)\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// if that fails, just save the original src\n\t\t\t\t\t\tassetSrcToSave = record.props.src\n\t\t\t\t\t}\n\n\t\t\t\t\tassets.push({\n\t\t\t\t\t\t...record,\n\t\t\t\t\t\tprops: {\n\t\t\t\t\t\t\t...record.props,\n\t\t\t\t\t\t\tsrc: assetSrcToSave,\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t} else {\n\t\t\t\t\tassets.push(record)\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\tcase 'shape':\n\t\t\t\tif ('assetId' in record.props) {\n\t\t\t\t\tusedAssets.add(record.props.assetId)\n\t\t\t\t}\n\t\t\t\trecords.push(record)\n\t\t\t\tbreak\n\t\t\tdefault:\n\t\t\t\trecords.push(record)\n\t\t\t\tbreak\n\t\t}\n\t}\n\tconst recordsToSave = records.concat(assets.filter((a) => usedAssets.has(a.id)))\n\n\treturn JSON.stringify({\n\t\ttldrawFileFormatVersion: LATEST_TLDRAW_FILE_FORMAT_VERSION,\n\t\tschema: store.schema.serialize(),\n\t\trecords: recordsToSave,\n\t})\n}\n\n/** @public */\nexport async function serializeTldrawJsonBlob(store: TLStore): Promise {\n\treturn new Blob([await serializeTldrawJson(store)], { type: TLDRAW_FILE_MIMETYPE })\n}\n\n/** @internal */\nexport async function parseAndLoadDocument(\n\teditor: Editor,\n\tdocument: string,\n\tmsg: (id: TLUiTranslationKey) => string,\n\taddToast: TLUiToastsContextType['addToast'],\n\tonV1FileLoad?: () => void,\n\tforceDarkMode?: boolean\n) {\n\tconst parseFileResult = parseTldrawJsonFile({\n\t\tschema: editor.store.schema,\n\t\tjson: document,\n\t})\n\tif (!parseFileResult.ok) {\n\t\tlet description\n\t\tswitch (parseFileResult.error.type) {\n\t\t\tcase 'notATldrawFile':\n\t\t\t\teditor.annotateError(parseFileResult.error.cause, {\n\t\t\t\t\torigin: 'file-system.open.parse',\n\t\t\t\t\twillCrashApp: false,\n\t\t\t\t\ttags: { parseErrorType: parseFileResult.error.type },\n\t\t\t\t})\n\t\t\t\treportError(parseFileResult.error.cause)\n\t\t\t\tdescription = msg('file-system.file-open-error.not-a-tldraw-file')\n\t\t\t\tbreak\n\t\t\tcase 'fileFormatVersionTooNew':\n\t\t\t\tdescription = msg('file-system.file-open-error.file-format-version-too-new')\n\t\t\t\tbreak\n\t\t\tcase 'migrationFailed':\n\t\t\t\tif (parseFileResult.error.reason === MigrationFailureReason.TargetVersionTooNew) {\n\t\t\t\t\tdescription = msg('file-system.file-open-error.file-format-version-too-new')\n\t\t\t\t} else {\n\t\t\t\t\tdescription = msg('file-system.file-open-error.generic-corrupted-file')\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\tcase 'invalidRecords':\n\t\t\t\teditor.annotateError(parseFileResult.error.cause, {\n\t\t\t\t\torigin: 'file-system.open.parse',\n\t\t\t\t\twillCrashApp: false,\n\t\t\t\t\ttags: { parseErrorType: parseFileResult.error.type },\n\t\t\t\t})\n\t\t\t\treportError(parseFileResult.error.cause)\n\t\t\t\tdescription = msg('file-system.file-open-error.generic-corrupted-file')\n\t\t\t\tbreak\n\t\t\tcase 'v1File': {\n\t\t\t\tbuildFromV1Document(editor, parseFileResult.error.data.document)\n\t\t\t\tonV1FileLoad?.()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\texhaustiveSwitchError(parseFileResult.error, 'type')\n\t\t}\n\t\taddToast({\n\t\t\ttitle: msg('file-system.file-open-error.title'),\n\t\t\tdescription,\n\t\t})\n\n\t\treturn\n\t}\n\n\t// tldraw file contain the full state of the app,\n\t// including ephemeral data. it up to the opener to\n\t// decide what to restore and what to retain. Here, we\n\t// just restore everything, so if the user has opened\n\t// this file before they'll get their camera etc.\n\t// restored. we could change this in the future.\n\ttransact(() => {\n\t\teditor.store.clear()\n\t\tconst [shapes, nonShapes] = partition(\n\t\t\tparseFileResult.value.allRecords(),\n\t\t\t(record) => record.typeName === 'shape'\n\t\t)\n\t\teditor.store.put(nonShapes, 'initialize')\n\t\teditor.store.ensureStoreIsUsable()\n\t\teditor.store.put(shapes, 'initialize')\n\t\teditor.history.clear()\n\t\teditor.updateViewportScreenBounds()\n\t\teditor.updateRenderingBounds()\n\n\t\tconst bounds = editor.currentPageBounds\n\t\tif (bounds) {\n\t\t\teditor.zoomToBounds(bounds, 1)\n\t\t}\n\t})\n\n\tif (forceDarkMode) editor.user.updateUserPreferences({ isDarkMode: true })\n}\n"], "mappings": "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", "names": ["e"] }