diff --git a/.changeset/big-cars-serve.md b/.changeset/big-cars-serve.md new file mode 100644 index 0000000000..ada0f61ba9 --- /dev/null +++ b/.changeset/big-cars-serve.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: transform textarea and contenteditable binding expressions diff --git a/.changeset/forty-peaches-unite.md b/.changeset/forty-peaches-unite.md new file mode 100644 index 0000000000..b24993deb2 --- /dev/null +++ b/.changeset/forty-peaches-unite.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: ensure unstate() only deeply applies to plain objects and arrays diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index d94e1f9fe6..85bf8dd278 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -29,9 +29,8 @@ export interface ComponentClientTransformState extends ClientTransformState { readonly hoisted: Array; readonly events: Set; - /** Stuff that happens before the render effect */ + /** Stuff that happens before the render effect(s) */ readonly init: Statement[]; - /** Stuff that happens inside separate render effects (due to call expressions) */ readonly update_effects: Statement[]; /** Stuff that happens inside the render effect */ @@ -42,7 +41,7 @@ export interface ComponentClientTransformState extends ClientTransformState { /** Used if condition for singular prop is false (see comment above) */ grouped: Statement; }[]; - /** Stuff that happens after the render effect (bindings, actions) */ + /** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */ readonly after_update: Statement[]; /** The HTML template string */ readonly template: string[]; 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 4c9115f09a..cf028e5130 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 @@ -1802,11 +1802,11 @@ export const template_visitors = { } if (is_reactive) { - context.state.init.push( + context.state.after_update.push( b.stmt(b.call('$.snippet_effect', b.thunk(snippet_function), ...args)) ); } else { - context.state.init.push(b.stmt(b.call(snippet_function, ...args))); + context.state.after_update.push(b.stmt(b.call(snippet_function, ...args))); } }, AnimateDirective(node, { state, visit }) { @@ -2604,16 +2604,14 @@ export const template_visitors = { ); } - const getter = b.thunk( - /** @type {import('estree').Expression} */ (context.visit(node.expression)) - ); + const getter = b.thunk(/** @type {import('estree').Expression} */ (visit(node.expression))); const assignment = b.assignment('=', node.expression, b.id('$$value')); const setter = b.arrow( [b.id('$$value')], serialize_set_binding( assignment, context, - () => /** @type {import('estree').Expression} */ (context.visit(assignment)), + () => /** @type {import('estree').Expression} */ (visit(assignment)), { skip_proxy_and_freeze: true } @@ -2776,9 +2774,7 @@ export const template_visitors = { group_getter = b.thunk( b.block([ b.stmt(serialize_attribute_value(value, context)[1]), - b.return( - /** @type {import('estree').Expression} */ (context.visit(node.expression)) - ) + b.return(/** @type {import('estree').Expression} */ (visit(node.expression))) ]) ); } @@ -2987,7 +2983,7 @@ export const template_visitors = { : b.member(b.member(b.id('$$props'), b.id('$$slots')), name, true, true); const slot = b.call('$.slot', context.state.node, expression, props_expression, fallback); - context.state.init.push(b.stmt(slot)); + context.state.after_update.push(b.stmt(slot)); }, SvelteHead(node, context) { // TODO attributes? diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index f6fab25b9c..680d73d96d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -1445,9 +1445,6 @@ const template_visitors = { context.state.init.push(b.stmt(b.call('$.add_snippet_symbol', node.expression))); } }, - BindDirective(node, context) { - // TODO - }, Component(node, context) { const state = context.state; const [dec, id] = serialize_anchor(state); @@ -1687,9 +1684,19 @@ function serialize_element_attributes(node, context) { if (binding?.omit_in_ssr) continue; if (ContentEditableBindings.includes(attribute.name)) { - content = { escape: false, expression: attribute.expression }; + content = { + escape: false, + expression: /** @type {import('estree').Expression} */ ( + context.visit(attribute.expression) + ) + }; } else if (attribute.name === 'value' && node.name === 'textarea') { - content = { escape: true, expression: attribute.expression }; + content = { + escape: true, + expression: /** @type {import('estree').Expression} */ ( + context.visit(attribute.expression) + ) + }; } else if (attribute.name === 'group') { const value_attribute = /** @type {import('#compiler').Attribute | undefined} */ ( node.attributes.find((attr) => attr.type === 'Attribute' && attr.name === 'value') diff --git a/packages/svelte/src/internal/client/proxy/proxy.js b/packages/svelte/src/internal/client/proxy/proxy.js index ae993267d6..d0bb285e10 100644 --- a/packages/svelte/src/internal/client/proxy/proxy.js +++ b/packages/svelte/src/internal/client/proxy/proxy.js @@ -11,11 +11,15 @@ import { batch_inspect } from '../runtime.js'; import { + array_prototype, define_property, get_descriptor, get_descriptors, + get_prototype_of, is_array, - object_keys + is_frozen, + object_keys, + object_prototype } from '../utils.js'; /** @typedef {{ s: Map>; v: import('../types.js').SourceSignal; a: boolean, i: boolean, p: StateObject }} Metadata */ @@ -23,12 +27,6 @@ import { export const STATE_SYMBOL = Symbol('$state'); export const READONLY_SYMBOL = Symbol('readonly'); - -const object_prototype = Object.prototype; -const array_prototype = Array.prototype; -const get_prototype_of = Object.getPrototypeOf; -const is_frozen = Object.isFrozen; - /** * @template {StateObject} T * @param {T} value diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index b4ef2f3e7f..de37dcdae9 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1,7 +1,16 @@ import { DEV } from 'esm-env'; import { subscribe_to_store } from '../../store/utils.js'; import { EMPTY_FUNC, run_all } from '../common.js'; -import { get_descriptor, get_descriptors, is_array, is_frozen, object_freeze } from './utils.js'; +import { + array_prototype, + get_descriptor, + get_descriptors, + get_prototype_of, + is_array, + is_frozen, + object_freeze, + object_prototype +} from './utils.js'; import { PROPS_IS_LAZY_INITIAL, PROPS_IS_IMMUTABLE, @@ -1975,19 +1984,23 @@ function deep_unstate(value, visited = new Map()) { visited.set(value, unstated); return unstated; } - - let contains_unstated = false; - /** @type {any} */ - const nested_unstated = Array.isArray(value) ? [] : {}; - for (let key in value) { - const result = deep_unstate(value[key], visited); - nested_unstated[key] = result; - if (result !== value[key]) { - contains_unstated = true; + const prototype = get_prototype_of(value); + // Only deeply unstate plain objects and arrays + if (prototype === object_prototype || prototype === array_prototype) { + let contains_unstated = false; + /** @type {any} */ + const nested_unstated = Array.isArray(value) ? [] : {}; + for (let key in value) { + const result = deep_unstate(value[key], visited); + nested_unstated[key] = result; + if (result !== value[key]) { + contains_unstated = true; + } } + visited.set(value, contains_unstated ? nested_unstated : value); + } else { + visited.set(value, value); } - - visited.set(value, contains_unstated ? nested_unstated : value); } return visited.get(value) ?? value; diff --git a/packages/svelte/src/internal/client/utils.js b/packages/svelte/src/internal/client/utils.js index 7c1b01515e..1da6f2fb39 100644 --- a/packages/svelte/src/internal/client/utils.js +++ b/packages/svelte/src/internal/client/utils.js @@ -10,6 +10,9 @@ export var object_freeze = Object.freeze; export var define_property = Object.defineProperty; export var get_descriptor = Object.getOwnPropertyDescriptor; export var get_descriptors = Object.getOwnPropertyDescriptors; +export var object_prototype = Object.prototype; +export var array_prototype = Array.prototype; +export var get_prototype_of = Object.getPrototypeOf; /** * @param {any} thing diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-store/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-store/_config.js index 81c83d73de..b8c4ab1f9a 100644 --- a/packages/svelte/tests/runtime-legacy/samples/binding-store/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/binding-store/_config.js @@ -4,11 +4,15 @@ export default test({ html: `

hello world

+ +
world
`, ssrHtml: `

hello world

+ +
world
`, async test({ assert, component, target, window }) { @@ -34,6 +38,8 @@ export default test({ `

hello everybody

+ +
everybody
` ); @@ -44,6 +50,8 @@ export default test({ `

hello goodbye

+ +
goodbye
` ); diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-store/main.svelte b/packages/svelte/tests/runtime-legacy/samples/binding-store/main.svelte index fd022a34b3..2bcf939af5 100644 --- a/packages/svelte/tests/runtime-legacy/samples/binding-store/main.svelte +++ b/packages/svelte/tests/runtime-legacy/samples/binding-store/main.svelte @@ -6,4 +6,6 @@ -

hello {$name}

\ No newline at end of file +

hello {$name}

+