chore: harden local schema hints and tests

This commit is contained in:
Ryan Di
2026-03-20 19:33:01 +11:00
parent eda7c8d6e9
commit 8168d46f87
4 changed files with 80 additions and 9 deletions
+25 -8
View File
@@ -54,6 +54,7 @@ import {
restoreElements,
} from "@excalidraw/excalidraw/data/restore";
import {
hasElementSchemaVersion,
migrateSceneElements,
SCHEMA_VERSIONS,
} from "@excalidraw/excalidraw/data/schema";
@@ -231,6 +232,17 @@ const initializeScene = async (opts: {
const externalUrlMatch = window.location.hash.match(/^#url=(.*)$/);
const localDataState = importFromLocalStorage();
const localStorageSchemaVersionSource = hasElementSchemaVersion(
localDataState?.elements,
)
? {
payloadSchemaVersion: undefined,
fallbackVersion: SCHEMA_VERSIONS.initial,
}
: {
payloadSchemaVersion: localDataState?.schemaVersion,
fallbackVersion: SCHEMA_VERSIONS.initial,
};
let scene: Omit<
RestoredDataState,
@@ -243,10 +255,7 @@ const initializeScene = async (opts: {
elements: restoreElements(
migrateSceneElements(
localDataState?.elements,
{
payloadSchemaVersion: localDataState?.schemaVersion,
fallbackVersion: SCHEMA_VERSIONS.initial,
},
localStorageSchemaVersionSource,
),
null,
{
@@ -573,12 +582,20 @@ const ExcalidrawWrapper = () => {
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
const localDataState = importFromLocalStorage();
const username = importUsernameFromLocalStorage();
const localStorageSchemaVersionSource = hasElementSchemaVersion(
localDataState?.elements,
)
? {
payloadSchemaVersion: undefined,
fallbackVersion: SCHEMA_VERSIONS.initial,
}
: {
payloadSchemaVersion: localDataState?.schemaVersion,
fallbackVersion: SCHEMA_VERSIONS.initial,
};
const migratedElements = migrateSceneElements(
localDataState?.elements,
{
payloadSchemaVersion: localDataState?.schemaVersion,
fallbackVersion: SCHEMA_VERSIONS.initial,
},
localStorageSchemaVersionSource,
);
setLangCode(getPreferredLanguage());
excalidrawAPI.updateScene({
+6 -1
View File
@@ -87,9 +87,14 @@ const saveDataStateToLocalStorage = (
_appState.openSidebar = null;
}
const persistedElements = getNonDeletedElements(elements).map((element) => ({
...element,
schemaVersion: SCHEMA_VERSIONS.latest,
}));
localStorage.setItem(
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
JSON.stringify(getNonDeletedElements(elements)),
JSON.stringify(persistedElements),
);
localStorage.setItem(
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
+40
View File
@@ -3,6 +3,7 @@ import { DEFAULT_ELEMENT_PROPS } from "@excalidraw/common";
import { API } from "../tests/helpers/api";
import {
ALL_SCOPES,
hasElementSchemaVersion,
type SchemaMigration,
migrateAPIElements,
migrateClipboardElements,
@@ -179,6 +180,34 @@ describe("schema migration", () => {
expect(migrated[0].backgroundColor).toBe("#ff0000");
});
it("should migrate mixed-hint elements individually when payload schema is missing", () => {
const legacyFrame = API.createElement({
type: "frame",
backgroundColor: "#ff0000",
});
const modernFrame = API.createElement({
type: "frame",
backgroundColor: "#00ff00",
});
const modernFrameWithHint = {
...modernFrame,
schemaVersion: SCHEMA_VERSIONS.latest,
} as typeof modernFrame & { schemaVersion: number };
const migrated = migrateSceneElements(
[legacyFrame, modernFrameWithHint],
{
payloadSchemaVersion: undefined,
fallbackVersion: SCHEMA_VERSIONS.initial,
},
)!;
expect(migrated[0].backgroundColor).toBe(
DEFAULT_ELEMENT_PROPS.backgroundColor,
);
expect(migrated[1].backgroundColor).toBe("#00ff00");
});
it("should prefer payload schema over per-element schema", () => {
const frame = API.createElement({
type: "frame",
@@ -198,4 +227,15 @@ describe("schema migration", () => {
DEFAULT_ELEMENT_PROPS.backgroundColor,
);
});
it("should detect schema hints on elements", () => {
const frame = API.createElement({ type: "frame" });
const withHint = {
...frame,
schemaVersion: SCHEMA_VERSIONS.latest,
} as typeof frame & { schemaVersion: number };
expect(hasElementSchemaVersion([frame])).toBe(false);
expect(hasElementSchemaVersion([withHint])).toBe(true);
});
});
+9
View File
@@ -85,6 +85,15 @@ const isValidSchemaVersion = (
);
};
export const hasElementSchemaVersion = (
elements: readonly ExcalidrawElement[] | null | undefined,
) =>
!!elements?.some((element) =>
isValidSchemaVersion(
(element as ExcalidrawElement & { schemaVersion?: number }).schemaVersion,
),
);
export const validateSchemaMigrations = (
migrations: readonly SchemaMigration[],
) => {