fix: make Object.hasOwn reactive for state proxies (#18838)

Fixes #18837

### What changed

State proxies now establish a per-property reactive dependency when
`Object.hasOwn(proxy, prop)` invokes the `getOwnPropertyDescriptor`
trap. Property additions and deletions therefore invalidate the
expression, consistently with the `prop in proxy` operator.

The source creation uses the same eligibility constraints as the
existing `has` trap so inherited properties retain their existing
semantics.

A runtime-runes regression sample covers both adding and deleting the
observed property in DOM and hydration modes.

### Tests and linting

- `FILTER=object-has-own-reactive pnpm test runtime-runes
--reporter=dot` — 4 passed
- `pnpm exec vitest run
packages/svelte/src/internal/client/proxy.test.ts --reporter=dot` — 8
passed
- `pnpm test runtime-runes --reporter=dot` — 2707 passed, 34 skipped
- `pnpm test --reporter=dot` — 7786 passed, 55 skipped
- `cd packages/svelte && pnpm check` — passed
- `pnpm format` — passed
- `pnpm lint` — passed

Includes a patch changeset for `svelte`.

<!--
svelte-triage-bot:feedback-baseline:81ef2357a99a2592312d5483e38be3f6ddeb4be5
-->

---------

Co-authored-by: svelte-triage-bot <team@svelte.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/18042/merge
svelte-triage-bot[bot] 4 days ago committed by GitHub
parent 6eb720a1b7
commit a72dc8eadb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make Object.hasOwn reactive for state proxy ownership changes

@ -204,16 +204,21 @@ export function proxy(value) {
},
getOwnPropertyDescriptor(target, prop) {
this.has?.(target, prop);
var descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
var s = sources.get(prop);
if (descriptor && 'value' in descriptor) {
var s = sources.get(prop);
if (s) descriptor.value = get(s);
} else if (descriptor === undefined) {
var source = sources.get(prop);
var value = source?.v;
if (s !== undefined) {
var value = get(s);
if (value === UNINITIALIZED) {
return undefined;
}
if (source !== undefined && value !== UNINITIALIZED) {
if (descriptor && 'value' in descriptor) {
descriptor.value = value;
} else {
return {
enumerable: true,
configurable: true,

@ -0,0 +1,24 @@
import { test } from '../../test';
import { tick } from 'svelte';
export default test({
html: `<button>add y</button><button>delete y</button><p>false</p>`,
async test({ assert, target }) {
const [add, remove] = target.querySelectorAll('button');
add.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>add y</button><button>delete y</button><p>true</p>`
);
remove.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>add y</button><button>delete y</button><p>false</p>`
);
}
});

@ -0,0 +1,7 @@
<script>
let state = $state({});
</script>
<button onclick={() => (state.y = true)}>add y</button>
<button onclick={() => delete state.y}>delete y</button>
<p>{Object.hasOwn(state, 'y')}</p>
Loading…
Cancel
Save