From 6ac153ef595f823ec29a131fa00143fcbd837a28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89verton=20Toffanetto?= Date: Thu, 5 Nov 2026 00:37:29 -0300 Subject: [PATCH] fix(store): support null-prototype objects on the server --- .changeset/server-null-prototype-stores.md | 5 +++ packages/solid/store/src/server.ts | 3 +- .../test/server/null-prototype-store.spec.ts | 41 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 .changeset/server-null-prototype-stores.md create mode 100644 packages/solid/test/server/null-prototype-store.spec.ts diff --git a/.changeset/server-null-prototype-stores.md b/.changeset/server-null-prototype-stores.md new file mode 100644 index 0000000000..0d3aa1ecc5 --- /dev/null +++ b/.changeset/server-null-prototype-stores.md @@ -0,0 +1,5 @@ +--- +"solid-js": patch +--- + +Support null-prototype objects in server store producers, reconciliation, and nested partial updates. diff --git a/packages/solid/store/src/server.ts b/packages/solid/store/src/server.ts index 127c908545..5b007fb1f9 100644 --- a/packages/solid/store/src/server.ts +++ b/packages/solid/store/src/server.ts @@ -18,10 +18,11 @@ export type { export const $RAW = Symbol("state-raw"); export function isWrappable(obj: any) { + let proto; return ( obj != null && typeof obj === "object" && - (Object.getPrototypeOf(obj) === Object.prototype || Array.isArray(obj)) + (!(proto = Object.getPrototypeOf(obj)) || proto === Object.prototype || Array.isArray(obj)) ); } diff --git a/packages/solid/test/server/null-prototype-store.spec.ts b/packages/solid/test/server/null-prototype-store.spec.ts new file mode 100644 index 0000000000..9c629c7e3b --- /dev/null +++ b/packages/solid/test/server/null-prototype-store.spec.ts @@ -0,0 +1,41 @@ +import { describe, expect, test } from "vitest"; +import { createStore, produce, reconcile } from "../../store/src/server.js"; + +describe("null-prototype stores on the server", () => { + test("runs producers on a null-prototype object", () => { + const original = Object.assign(Object.create(null), { count: 1 }); + const [state, setState] = createStore<{ count: number }>(original); + + setState(produce(state => state.count++)); + + expect(state.count).toBe(2); + expect(state).toBe(original); + expect(Object.getPrototypeOf(state)).toBeNull(); + }); + + test("reconciles removed properties of a null-prototype object", () => { + const original = Object.assign(Object.create(null), { count: 1, removed: true }); + const [state, setState] = createStore<{ count: number; removed?: boolean }>(original); + + setState(reconcile({ count: 2 })); + + expect(state.count).toBe(2); + expect("removed" in state).toBe(false); + expect(state).toBe(original); + expect(Object.getPrototypeOf(state)).toBeNull(); + }); + + test("merges partial updates into a nested null-prototype object", () => { + const original = Object.assign(Object.create(null), { count: 1, label: "kept" }); + const [state, setState] = createStore<{ data: { count: number; label: string } }>({ + data: original + }); + + setState("data", { count: 2 }); + + expect(state.data.count).toBe(2); + expect(state.data.label).toBe("kept"); + expect(state.data).toBe(original); + expect(Object.getPrototypeOf(state.data)).toBeNull(); + }); +});