From f97e08ff94247aa6b6c6275c9c280ad4f79e505e Mon Sep 17 00:00:00 2001 From: Yarchik Date: Wed, 1 Jul 2026 23:37:46 +0100 Subject: [PATCH] fix(solid): exclude shared keys from later splitProps groups on proxy path The proxy branch of splitProps built one Proxy per group whose get/has/keys each tested membership against that group's key list independently, with no cross-group exclusion. A key listed in two groups was therefore exposed by both proxied groups, while the plain-object path assigns each key to the first group that lists it (first-match break). Component props and stores are proxies at runtime, so the proxy path is the live one. Track keys claimed by earlier groups and let each group own only its not-yet-claimed keys, so get, has, and keys all honor the exclusion and match the plain-object semantics. --- packages/solid/src/render/component.ts | 9 ++++++--- packages/solid/test/component.spec.ts | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/solid/src/render/component.ts b/packages/solid/src/render/component.ts index 5775162f8..b3cb707c5 100644 --- a/packages/solid/src/render/component.ts +++ b/packages/solid/src/render/component.ts @@ -293,17 +293,20 @@ export function splitProps< if (SUPPORTS_PROXY && $PROXY in props) { const blocked = len > 1 ? keys.flat() : keys[0]; + const claimed = new Set(); const res = keys.map(k => { + // a key belongs to the first group that lists it (matches non-proxy path) + const owned = k.filter(property => !claimed.has(property) && (claimed.add(property), true)); return new Proxy( { get(property) { - return k.includes(property) ? props[property as any] : undefined; + return owned.includes(property) ? props[property as any] : undefined; }, has(property) { - return k.includes(property) && property in props; + return owned.includes(property) && property in props; }, keys() { - return k.filter(property => property in props); + return owned.filter(property => property in props); } }, propTraps diff --git a/packages/solid/test/component.spec.ts b/packages/solid/test/component.spec.ts index 1d4424932..eb4b65767 100644 --- a/packages/solid/test/component.spec.ts +++ b/packages/solid/test/component.spec.ts @@ -417,6 +417,31 @@ describe("SplitProps Props", () => { expect(otherProps.id).toBe("input"); expect(Object.keys(otherProps).length).toBe(1); }); + test("SplitProps with keys shared across groups assigns to first group only", () => { + // A key listed in more than one group must belong to the first group that + // lists it, for both plain-object and proxy (store/props) sources. + const check = (a: any, b: any, rest: any) => { + expect(a.b).toBe(2); + expect("b" in a).toBe(true); + expect(Object.keys(a).sort()).toEqual(["a", "b"]); + + expect(b.b).toBeUndefined(); + expect("b" in b).toBe(false); + expect(Object.keys(b)).toEqual(["c"]); + expect({ ...b }).toEqual({ c: 3 }); + + expect({ ...rest }).toEqual({}); + }; + + const plain = splitProps({ a: 1, b: 2, c: 3 }, ["a", "b"], ["b", "c"]); + check(plain[0], plain[1], plain[2]); + + createRoot(() => { + const [state] = createStore({ a: 1, b: 2, c: 3 }); + const proxied = splitProps(state, ["a", "b"], ["b", "c"]); + check(proxied[0], proxied[1], proxied[2]); + }); + }); test("SplitProps returns same prop descriptors", () => { const inProps = { a: 1,