Skip to content

Commit f0d3740

Browse files
committed
fix: remove readonly validation
The readonly dev time validation results in false-negative object equality checks: The original object is proxified and thus comparisons could be between the unproxified and proxified version, which will always return false. Fixes #10372 There's also the problem that an object could be passed into a component but then passed upwards into shared state. At that point, it should no longer be readonly, but it's not possible to statically analyze these points. Fixes #10372 Lastly, the each block logic mutates an internal array and that also throws errors with the readonly logic. Fixes #10037
1 parent d08e05b commit f0d3740

File tree

24 files changed

+131
-259
lines changed

24 files changed

+131
-259
lines changed

.changeset/tough-houses-sparkle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
fix: remove readonly validation as it results in false-negative object equality checks

packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -835,8 +835,6 @@ function serialize_inline_component(node, component_name, context) {
835835
arg = b.call('$.get', id);
836836
}
837837

838-
if (context.state.options.dev) arg = b.call('$.readonly', arg);
839-
840838
push_prop(b.get(attribute.name, [b.return(arg)]));
841839
} else {
842840
push_prop(b.init(attribute.name, value));
@@ -2336,17 +2334,16 @@ export const template_visitors = {
23362334
/** @type {import('estree').BlockStatement} */ (context.visit(node.fallback))
23372335
)
23382336
: b.literal(null);
2339-
const key_function =
2340-
node.key && ((each_type & EACH_ITEM_REACTIVE) !== 0 || context.state.options.dev)
2341-
? b.arrow(
2342-
[node.context.type === 'Identifier' ? node.context : b.id('$$item'), index],
2343-
b.block(
2344-
declarations.concat(
2345-
b.return(/** @type {import('estree').Expression} */ (context.visit(node.key)))
2346-
)
2337+
const key_function = node.key
2338+
? b.arrow(
2339+
[node.context.type === 'Identifier' ? node.context : b.id('$$item'), index],
2340+
b.block(
2341+
declarations.concat(
2342+
b.return(/** @type {import('estree').Expression} */ (context.visit(node.key)))
23472343
)
23482344
)
2349-
: b.literal(null);
2345+
)
2346+
: b.literal(null);
23502347

23512348
if (node.index && each_node_meta.contains_group_binding) {
23522349
// We needed to create a unique identifier for the index above, but we want to use the

packages/svelte/src/internal/client/proxy.js

Lines changed: 1 addition & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,10 @@ import {
1818
get_prototype_of,
1919
is_array,
2020
is_frozen,
21-
object_keys,
2221
object_prototype
2322
} from './utils.js';
2423

2524
export const STATE_SYMBOL = Symbol('$state');
26-
export const READONLY_SYMBOL = Symbol('readonly');
2725

2826
/**
2927
* @template T
@@ -93,7 +91,7 @@ function unwrap(value, already_unwrapped = new Map()) {
9391
const descriptors = get_descriptors(value);
9492
already_unwrapped.set(value, obj);
9593
for (const key of keys) {
96-
if (key === STATE_SYMBOL || (DEV && key === READONLY_SYMBOL)) continue;
94+
if (key === STATE_SYMBOL) continue;
9795
if (descriptors[key].get) {
9896
define_property(obj, key, descriptors[key]);
9997
} else {
@@ -175,9 +173,6 @@ const state_proxy_handler = {
175173
},
176174

177175
get(target, prop, receiver) {
178-
if (DEV && prop === READONLY_SYMBOL) {
179-
return Reflect.get(target, READONLY_SYMBOL);
180-
}
181176
if (prop === STATE_SYMBOL) {
182177
return Reflect.get(target, STATE_SYMBOL);
183178
}
@@ -224,9 +219,6 @@ const state_proxy_handler = {
224219
},
225220

226221
has(target, prop) {
227-
if (DEV && prop === READONLY_SYMBOL) {
228-
return Reflect.has(target, READONLY_SYMBOL);
229-
}
230222
if (prop === STATE_SYMBOL) {
231223
return true;
232224
}
@@ -250,10 +242,6 @@ const state_proxy_handler = {
250242
},
251243

252244
set(target, prop, value) {
253-
if (DEV && prop === READONLY_SYMBOL) {
254-
target[READONLY_SYMBOL] = value;
255-
return true;
256-
}
257245
const metadata = target[STATE_SYMBOL];
258246
const s = metadata.s.get(prop);
259247
if (s !== undefined) set(s, proxy(value, metadata.i));
@@ -304,69 +292,3 @@ if (DEV) {
304292
throw new Error('Cannot set prototype of $state object');
305293
};
306294
}
307-
308-
/**
309-
* Expects a value that was wrapped with `proxy` and makes it readonly.
310-
*
311-
* @template {Record<string | symbol, any>} T
312-
* @template {import('./types.js').ProxyReadonlyObject<T> | T} U
313-
* @param {U} value
314-
* @returns {Proxy<U> | U}
315-
*/
316-
export function readonly(value) {
317-
const proxy = value && value[READONLY_SYMBOL];
318-
if (proxy) {
319-
const metadata = value[STATE_SYMBOL];
320-
// Check that the incoming value is the same proxy that this readonly symbol was created for:
321-
// If someone copies over the readonly symbol to a new object (using Reflect.ownKeys) the referenced
322-
// proxy could be stale and we should not return it.
323-
if (metadata.p === value) return proxy;
324-
}
325-
326-
if (
327-
typeof value === 'object' &&
328-
value != null &&
329-
!is_frozen(value) &&
330-
STATE_SYMBOL in value && // TODO handle Map and Set as well
331-
!(READONLY_SYMBOL in value)
332-
) {
333-
const proxy = new Proxy(
334-
value,
335-
/** @type {ProxyHandler<import('./types.js').ProxyReadonlyObject<U>>} */ (
336-
readonly_proxy_handler
337-
)
338-
);
339-
define_property(value, READONLY_SYMBOL, { value: proxy, writable: false });
340-
return proxy;
341-
}
342-
343-
return value;
344-
}
345-
346-
/**
347-
* @param {any} _
348-
* @param {string} prop
349-
* @returns {never}
350-
*/
351-
const readonly_error = (_, prop) => {
352-
throw new Error(
353-
`Non-bound props cannot be mutated — to make the \`${prop}\` settable, ensure the object it is used within is bound as a prop \`bind:<prop>={...}\`. Fallback values can never be mutated.`
354-
);
355-
};
356-
357-
/** @type {ProxyHandler<import('./types.js').ProxyReadonlyObject>} */
358-
const readonly_proxy_handler = {
359-
defineProperty: readonly_error,
360-
deleteProperty: readonly_error,
361-
set: readonly_error,
362-
363-
get(target, prop, receiver) {
364-
const value = Reflect.get(target, prop, receiver);
365-
366-
if (!(prop in target)) {
367-
return readonly(value);
368-
}
369-
370-
return value;
371-
}
372-
};

packages/svelte/src/internal/client/runtime.js

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
PROPS_IS_RUNES,
1818
PROPS_IS_UPDATED
1919
} from '../../constants.js';
20-
import { READONLY_SYMBOL, STATE_SYMBOL, proxy, readonly, unstate } from './proxy.js';
20+
import { STATE_SYMBOL, unstate } from './proxy.js';
2121
import { EACH_BLOCK, IF_BLOCK } from './block.js';
2222

2323
export const SOURCE = 1;
@@ -1619,10 +1619,6 @@ export function prop(props, key, flags, initial) {
16191619
// @ts-expect-error would need a cumbersome method overload to type this
16201620
if ((flags & PROPS_IS_LAZY_INITIAL) !== 0) initial = initial();
16211621

1622-
if (DEV && runes) {
1623-
initial = readonly(proxy(/** @type {any} */ (initial)));
1624-
}
1625-
16261622
prop_value = /** @type {V} */ (initial);
16271623

16281624
if (setter) setter(prop_value);
@@ -2126,10 +2122,6 @@ export function freeze(value) {
21262122
if (STATE_SYMBOL in value) {
21272123
return object_freeze(unstate(value));
21282124
}
2129-
// If the value is already read-only then just use that
2130-
if (DEV && READONLY_SYMBOL in value) {
2131-
return value;
2132-
}
21332125
// Otherwise freeze the object
21342126
object_freeze(value);
21352127
}

packages/svelte/src/internal/client/types.d.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
DYNAMIC_ELEMENT_BLOCK,
1111
SNIPPET_BLOCK
1212
} from './block.js';
13-
import type { READONLY_SYMBOL, STATE_SYMBOL } from './proxy.js';
13+
import type { STATE_SYMBOL } from './proxy.js';
1414
import { DERIVED, EFFECT, RENDER_EFFECT, SOURCE, PRE_EFFECT, LAZY_PROPERTY } from './runtime.js';
1515

1616
// Put all internal types in this file. Once we convert to JSDoc, we can make this a d.ts file
@@ -409,15 +409,11 @@ export interface ProxyMetadata<T = Record<string | symbol, any>> {
409409
/** Immutable: Whether to use a source or mutable source under the hood */
410410
i: boolean;
411411
/** The associated proxy */
412-
p: ProxyStateObject<T> | ProxyReadonlyObject<T>;
412+
p: ProxyStateObject<T>;
413413
/** The original target this proxy was created for */
414414
t: T;
415415
}
416416

417417
export type ProxyStateObject<T = Record<string | symbol, any>> = T & {
418418
[STATE_SYMBOL]: ProxyMetadata;
419419
};
420-
421-
export type ProxyReadonlyObject<T = Record<string | symbol, any>> = ProxyStateObject<T> & {
422-
[READONLY_SYMBOL]: ProxyMetadata;
423-
};

packages/svelte/src/internal/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export * from './client/each.js';
4444
export * from './client/render.js';
4545
export * from './client/validate.js';
4646
export { raf } from './client/timing.js';
47-
export { proxy, readonly, unstate } from './client/proxy.js';
47+
export { proxy, unstate } from './client/proxy.js';
4848
export { create_custom_element } from './client/custom-element.js';
4949
export {
5050
child,
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: `<button>add</button> <p>1</p><p>1</p><p>1</p>`,
5+
6+
async test({ assert, target }) {
7+
const btn = target.querySelector('button');
8+
9+
await btn?.click();
10+
assert.htmlEqual(
11+
target.innerHTML,
12+
`<button>add</button> <p>1</p><p>2</p><p>1</p><p>2</p><p>1</p><p>2</p>`
13+
);
14+
}
15+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script>
2+
let { array } = $props();
3+
</script>
4+
5+
{#each array as number}
6+
<p>{number.v}</p>
7+
{/each}
8+
9+
{#each array as number (number)}
10+
<p>{number.v}</p>
11+
{/each}
12+
13+
{#each array as number (number.v)}
14+
<p>{number.v}</p>
15+
{/each}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script>
2+
import Child from './child.svelte';
3+
4+
let array = $state([{v: 1}]);
5+
6+
const addNew = () => {
7+
array.push({v: 2})
8+
}
9+
</script>
10+
11+
<button onclick={addNew}>add</button>
12+
<Child {array} />
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: `
5+
<button>a true</button><button>b true</button>
6+
<button>a true</button><button>b true</button>
7+
<button>a true</button><button>b true</button>
8+
`,
9+
10+
async test({ assert, target }) {
11+
let [btn1, _btn2, btn3, _btn4, btn5] = target.querySelectorAll('button');
12+
13+
await btn1.click();
14+
assert.htmlEqual(
15+
target.innerHTML,
16+
`
17+
<button>a+ true</button><button>b true</button>
18+
<button>a+ true</button><button>b true</button>
19+
<button>a+ true</button><button>b true</button>
20+
`
21+
);
22+
23+
[btn1, _btn2, btn3, _btn4, btn5] = target.querySelectorAll('button');
24+
await btn3.click();
25+
assert.htmlEqual(
26+
target.innerHTML,
27+
`
28+
<button>a++ true</button><button>b true</button>
29+
<button>a++ true</button><button>b true</button>
30+
<button>a++ true</button><button>b true</button>
31+
`
32+
);
33+
34+
[btn1, _btn2, btn3, _btn4, btn5] = target.querySelectorAll('button');
35+
await btn5.click();
36+
assert.htmlEqual(
37+
target.innerHTML,
38+
`
39+
<button>a+++ true</button><button>b true</button>
40+
<button>a+++ true</button><button>b true</button>
41+
<button>a+++ true</button><button>b true</button>
42+
`
43+
);
44+
}
45+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
let {item, items, onclick} = $props()
3+
</script>
4+
5+
<button {onclick}>
6+
{item.name} {items.includes(item)}
7+
</button>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script>
2+
import Item from './item.svelte'
3+
4+
let items = $state([{name: 'a'}, {name: 'b'}]);
5+
</script>
6+
7+
<!-- test that each block doesn't mess with item identity -->
8+
9+
{#each items as item (item)}
10+
<Item {item} {items} onclick={() => item.name = item.name + '+'} />
11+
{/each}
12+
13+
{#each items as item (item.name)}
14+
<Item {item} {items} onclick={() => {console.log('hello'); item.name = item.name + '+'}} />
15+
{/each}
16+
17+
{#each items as item}
18+
<Item {item} {items} onclick={() => item.name = item.name + '+'} />
19+
{/each}

packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly-bail/Counter.svelte

Lines changed: 0 additions & 8 deletions
This file was deleted.

packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly-bail/_config.js

Lines changed: 0 additions & 22 deletions
This file was deleted.

0 commit comments

Comments
 (0)