diff --git a/.changeset/tough-houses-sparkle.md b/.changeset/tough-houses-sparkle.md new file mode 100644 index 000000000000..562b101834ff --- /dev/null +++ b/.changeset/tough-houses-sparkle.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: remove readonly validation as it results in false-negative object equality checks diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 5bef734b6329..ce9738a67b0e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -842,8 +842,6 @@ function serialize_inline_component(node, component_name, context) { arg = b.call('$.get', id); } - if (context.state.options.dev) arg = b.call('$.readonly', arg); - push_prop(b.get(attribute.name, [b.return(arg)])); } else { push_prop(b.init(attribute.name, value)); @@ -2376,17 +2374,16 @@ export const template_visitors = { /** @type {import('estree').BlockStatement} */ (context.visit(node.fallback)) ) : b.literal(null); - const key_function = - node.key && ((each_type & EACH_ITEM_REACTIVE) !== 0 || context.state.options.dev) - ? b.arrow( - [node.context.type === 'Identifier' ? node.context : b.id('$$item'), index], - b.block( - declarations.concat( - b.return(/** @type {import('estree').Expression} */ (context.visit(node.key))) - ) + const key_function = node.key + ? b.arrow( + [node.context.type === 'Identifier' ? node.context : b.id('$$item'), index], + b.block( + declarations.concat( + b.return(/** @type {import('estree').Expression} */ (context.visit(node.key))) ) ) - : b.literal(null); + ) + : b.literal(null); if (node.index && each_node_meta.contains_group_binding) { // We needed to create a unique identifier for the index above, but we want to use the diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 0fff0447c0f9..46f0859964ad 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -22,7 +22,6 @@ import { } from './utils.js'; export const STATE_SYMBOL = Symbol('$state'); -export const READONLY_SYMBOL = Symbol('readonly'); /** * @template T @@ -95,7 +94,7 @@ function unwrap(value, already_unwrapped) { already_unwrapped.set(value, obj); for (const key of keys) { - if (key === STATE_SYMBOL || (DEV && key === READONLY_SYMBOL)) continue; + if (key === STATE_SYMBOL) continue; if (descriptors[key].get) { define_property(obj, key, descriptors[key]); } else { @@ -163,9 +162,6 @@ const state_proxy_handler = { }, get(target, prop, receiver) { - if (DEV && prop === READONLY_SYMBOL) { - return Reflect.get(target, READONLY_SYMBOL); - } if (prop === STATE_SYMBOL) { return Reflect.get(target, STATE_SYMBOL); } @@ -212,9 +208,6 @@ const state_proxy_handler = { }, has(target, prop) { - if (DEV && prop === READONLY_SYMBOL) { - return Reflect.has(target, READONLY_SYMBOL); - } if (prop === STATE_SYMBOL) { return true; } @@ -238,10 +231,6 @@ const state_proxy_handler = { }, set(target, prop, value) { - if (DEV && prop === READONLY_SYMBOL) { - target[READONLY_SYMBOL] = value; - return true; - } const metadata = target[STATE_SYMBOL]; const s = metadata.s.get(prop); if (s !== undefined) set(s, proxy(value, metadata.i)); @@ -292,69 +281,3 @@ if (DEV) { throw new Error('Cannot set prototype of $state object'); }; } - -/** - * Expects a value that was wrapped with `proxy` and makes it readonly. - * - * @template {Record} T - * @template {import('./types.js').ProxyReadonlyObject | T} U - * @param {U} value - * @returns {Proxy | U} - */ -export function readonly(value) { - const proxy = value && value[READONLY_SYMBOL]; - if (proxy) { - const metadata = value[STATE_SYMBOL]; - // Check that the incoming value is the same proxy that this readonly symbol was created for: - // If someone copies over the readonly symbol to a new object (using Reflect.ownKeys) the referenced - // proxy could be stale and we should not return it. - if (metadata.p === value) return proxy; - } - - if ( - typeof value === 'object' && - value != null && - !is_frozen(value) && - STATE_SYMBOL in value && // TODO handle Map and Set as well - !(READONLY_SYMBOL in value) - ) { - const proxy = new Proxy( - value, - /** @type {ProxyHandler>} */ ( - readonly_proxy_handler - ) - ); - define_property(value, READONLY_SYMBOL, { value: proxy, writable: false }); - return proxy; - } - - return value; -} - -/** - * @param {any} _ - * @param {string} prop - * @returns {never} - */ -const readonly_error = (_, prop) => { - throw new Error( - `Non-bound props cannot be mutated — to make the \`${prop}\` settable, ensure the object it is used within is bound as a prop \`bind:={...}\`. Fallback values can never be mutated.` - ); -}; - -/** @type {ProxyHandler} */ -const readonly_proxy_handler = { - defineProperty: readonly_error, - deleteProperty: readonly_error, - set: readonly_error, - - get(target, prop, receiver) { - const value = Reflect.get(target, prop, receiver); - - if (!(prop in target)) { - return readonly(value); - } - - return value; - } -}; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index e200c6ec32b1..03b4615a672c 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -17,7 +17,7 @@ import { PROPS_IS_RUNES, PROPS_IS_UPDATED } from '../../constants.js'; -import { READONLY_SYMBOL, STATE_SYMBOL, proxy, readonly, unstate } from './proxy.js'; +import { STATE_SYMBOL, unstate } from './proxy.js'; import { EACH_BLOCK, IF_BLOCK } from './block.js'; export const SOURCE = 1; @@ -1580,10 +1580,6 @@ export function prop(props, key, flags, initial) { // @ts-expect-error would need a cumbersome method overload to type this if ((flags & PROPS_IS_LAZY_INITIAL) !== 0) initial = initial(); - if (DEV && runes) { - initial = readonly(proxy(/** @type {any} */ (initial))); - } - prop_value = /** @type {V} */ (initial); if (setter) setter(prop_value); @@ -2114,10 +2110,6 @@ export function freeze(value) { if (STATE_SYMBOL in value) { return object_freeze(unstate(value)); } - // If the value is already read-only then just use that - if (DEV && READONLY_SYMBOL in value) { - return value; - } // Otherwise freeze the object object_freeze(value); } diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 7d34789d3b5d..8996e92e4081 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -10,7 +10,7 @@ import { DYNAMIC_ELEMENT_BLOCK, SNIPPET_BLOCK } from './block.js'; -import type { READONLY_SYMBOL, STATE_SYMBOL } from './proxy.js'; +import type { STATE_SYMBOL } from './proxy.js'; import { DERIVED, EFFECT, RENDER_EFFECT, SOURCE, PRE_EFFECT } from './runtime.js'; // Put all internal types in this file. Once we convert to JSDoc, we can make this a d.ts file @@ -403,7 +403,7 @@ export interface ProxyMetadata> { /** Immutable: Whether to use a source or mutable source under the hood */ i: boolean; /** The associated proxy */ - p: ProxyStateObject | ProxyReadonlyObject; + p: ProxyStateObject; /** The original target this proxy was created for */ t: T; } @@ -411,7 +411,3 @@ export interface ProxyMetadata> { export type ProxyStateObject> = T & { [STATE_SYMBOL]: ProxyMetadata; }; - -export type ProxyReadonlyObject> = ProxyStateObject & { - [READONLY_SYMBOL]: ProxyMetadata; -}; diff --git a/packages/svelte/src/internal/index.js b/packages/svelte/src/internal/index.js index 9077585a3380..855fa43ced9a 100644 --- a/packages/svelte/src/internal/index.js +++ b/packages/svelte/src/internal/index.js @@ -44,7 +44,7 @@ export * from './client/each.js'; export * from './client/render.js'; export * from './client/validate.js'; export { raf } from './client/timing.js'; -export { proxy, readonly, unstate } from './client/proxy.js'; +export { proxy, unstate } from './client/proxy.js'; export { create_custom_element } from './client/custom-element.js'; export { child, diff --git a/packages/svelte/tests/runtime-runes/samples/props-array-each/_config.js b/packages/svelte/tests/runtime-runes/samples/props-array-each/_config.js new file mode 100644 index 000000000000..b3d63d3732e3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-array-each/_config.js @@ -0,0 +1,15 @@ +import { test } from '../../test'; + +export default test({ + html: `

1

1

1

`, + + async test({ assert, target }) { + const btn = target.querySelector('button'); + + await btn?.click(); + assert.htmlEqual( + target.innerHTML, + `

1

2

1

2

1

2

` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/props-array-each/child.svelte b/packages/svelte/tests/runtime-runes/samples/props-array-each/child.svelte new file mode 100644 index 000000000000..9f5a561f522c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-array-each/child.svelte @@ -0,0 +1,15 @@ + + +{#each array as number} +

{number.v}

+{/each} + +{#each array as number (number)} +

{number.v}

+{/each} + +{#each array as number (number.v)} +

{number.v}

+{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/props-array-each/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-array-each/main.svelte new file mode 100644 index 000000000000..05e36da61645 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-array-each/main.svelte @@ -0,0 +1,12 @@ + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/props-equality/_config.js b/packages/svelte/tests/runtime-runes/samples/props-equality/_config.js new file mode 100644 index 000000000000..eda378947bdd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-equality/_config.js @@ -0,0 +1,45 @@ +import { test } from '../../test'; + +export default test({ + html: ` + + + + `, + + async test({ assert, target }) { + let [btn1, _btn2, btn3, _btn4, btn5] = target.querySelectorAll('button'); + + await btn1.click(); + assert.htmlEqual( + target.innerHTML, + ` + + + + ` + ); + + [btn1, _btn2, btn3, _btn4, btn5] = target.querySelectorAll('button'); + await btn3.click(); + assert.htmlEqual( + target.innerHTML, + ` + + + + ` + ); + + [btn1, _btn2, btn3, _btn4, btn5] = target.querySelectorAll('button'); + await btn5.click(); + assert.htmlEqual( + target.innerHTML, + ` + + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/props-equality/item.svelte b/packages/svelte/tests/runtime-runes/samples/props-equality/item.svelte new file mode 100644 index 000000000000..c40c50d9ce0c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-equality/item.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/props-equality/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-equality/main.svelte new file mode 100644 index 000000000000..7c7ee0c31efb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-equality/main.svelte @@ -0,0 +1,19 @@ + + + + +{#each items as item (item)} + item.name = item.name + '+'} /> +{/each} + +{#each items as item (item.name)} + {console.log('hello'); item.name = item.name + '+'}} /> +{/each} + +{#each items as item} + item.name = item.name + '+'} /> +{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly-bail/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly-bail/Counter.svelte deleted file mode 100644 index 20f744f07d36..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly-bail/Counter.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly-bail/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly-bail/_config.js deleted file mode 100644 index 8726eead09cc..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly-bail/_config.js +++ /dev/null @@ -1,22 +0,0 @@ -import { test } from '../../test'; - -// Tests that readonly bails on setters/classes -export default test({ - html: ``, - - compileOptions: { - dev: true - }, - - async test({ assert, target }) { - const [btn1, btn2] = target.querySelectorAll('button'); - - await btn1.click(); - await btn2.click(); - assert.htmlEqual(target.innerHTML, ``); - - await btn1.click(); - await btn2.click(); - assert.htmlEqual(target.innerHTML, ``); - } -}); diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly-bail/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly-bail/main.svelte deleted file mode 100644 index 7ee12ca30613..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly-bail/main.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - - - diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly-reassigned/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly-reassigned/Counter.svelte deleted file mode 100644 index 1097b68f6f30..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly-reassigned/Counter.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly-reassigned/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly-reassigned/_config.js deleted file mode 100644 index 9787b1ca9271..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly-reassigned/_config.js +++ /dev/null @@ -1,19 +0,0 @@ -import { test } from '../../test'; - -export default test({ - html: ``, - - compileOptions: { - dev: true - }, - - async test({ assert, target }) { - const btn = target.querySelector('button'); - - await btn?.click(); - assert.htmlEqual(target.innerHTML, ``); - - await btn?.click(); - assert.htmlEqual(target.innerHTML, ``); - } -}); diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly-reassigned/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly-reassigned/main.svelte deleted file mode 100644 index 391afc4b232e..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly-reassigned/main.svelte +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly/Counter.svelte deleted file mode 100644 index 91ccc2af8188..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly/Counter.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly/_config.js deleted file mode 100644 index d78db389d74e..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly/_config.js +++ /dev/null @@ -1,19 +0,0 @@ -import { test } from '../../test'; - -export default test({ - html: ``, - - compileOptions: { - dev: true - }, - - async test({ assert, target }) { - const btn = target.querySelector('button'); - await btn?.click(); - - assert.htmlEqual(target.innerHTML, ``); - }, - - runtime_error: - 'Non-bound props cannot be mutated — to make the `count` settable, ensure the object it is used within is bound as a prop `bind:={...}`. Fallback values can never be mutated.' -}); diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly/main.svelte deleted file mode 100644 index 391afc4b232e..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly/main.svelte +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/Counter.svelte deleted file mode 100644 index d1be32683013..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/Counter.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/_config.js deleted file mode 100644 index d78db389d74e..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/_config.js +++ /dev/null @@ -1,19 +0,0 @@ -import { test } from '../../test'; - -export default test({ - html: ``, - - compileOptions: { - dev: true - }, - - async test({ assert, target }) { - const btn = target.querySelector('button'); - await btn?.click(); - - assert.htmlEqual(target.innerHTML, ``); - }, - - runtime_error: - 'Non-bound props cannot be mutated — to make the `count` settable, ensure the object it is used within is bound as a prop `bind:={...}`. Fallback values can never be mutated.' -}); diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/main.svelte deleted file mode 100644 index e9617af17d5c..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/main.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - -