diff --git a/.changeset/gorgeous-boxes-design.md b/.changeset/gorgeous-boxes-design.md new file mode 100644 index 000000000..5496f6f1f --- /dev/null +++ b/.changeset/gorgeous-boxes-design.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: ensure state update expressions are serialised correctly diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index c6b10bc2d..89a1a5000 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -119,10 +119,11 @@ export function serialize_get_binding(node, state) { * @param {import('estree').AssignmentExpression} node * @param {import('zimmerframe').Context} context * @param {() => any} fallback + * @param {boolean} prefix * @param {{skip_proxy_and_freeze?: boolean}} [options] * @returns {import('estree').Expression} */ -export function serialize_set_binding(node, context, fallback, options) { +export function serialize_set_binding(node, context, fallback, prefix, options) { const { state, visit } = context; const assignee = node.left; @@ -146,7 +147,9 @@ export function serialize_set_binding(node, context, fallback, options) { const value = path.expression?.(b.id(tmp_id)); const assignment = b.assignment('=', path.node, value); original_assignments.push(assignment); - assignments.push(serialize_set_binding(assignment, context, () => assignment, options)); + assignments.push( + serialize_set_binding(assignment, context, () => assignment, prefix, options) + ); } if (assignments.every((assignment, i) => assignment === original_assignments[i])) { @@ -411,6 +414,15 @@ export function serialize_set_binding(node, context, fallback, options) { ) ); } + } else if ( + node.right.type === 'Literal' && + (node.operator === '+=' || node.operator === '-=') + ) { + return b.update( + node.operator === '+=' ? '++' : '--', + /** @type {import('estree').Expression} */ (visit(node.left)), + prefix + ); } else { return b.assignment( node.operator, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js index e229b5edf..2834e9ff0 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js @@ -32,7 +32,7 @@ export const global_visitors = { next(); }, AssignmentExpression(node, context) { - return serialize_set_binding(node, context, context.next); + return serialize_set_binding(node, context, context.next, false); }, UpdateExpression(node, context) { const { state, next, visit } = context; @@ -98,7 +98,12 @@ export const global_visitors = { /** @type {import('estree').Pattern} */ (argument), b.literal(1) ); - const serialized_assignment = serialize_set_binding(assignment, context, () => assignment); + const serialized_assignment = serialize_set_binding( + assignment, + context, + () => assignment, + node.prefix + ); const value = /** @type {import('estree').Expression} */ (visit(argument)); if (serialized_assignment === assignment) { // No change to output -> nothing to transform -> we can keep the original update expression 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 d8ae6749f..823b92415 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 @@ -798,7 +798,9 @@ function serialize_inline_component(node, component_name, context) { const assignment = b.assignment('=', attribute.expression, b.id('$$value')); push_prop( b.set(attribute.name, [ - b.stmt(serialize_set_binding(assignment, context, () => context.visit(assignment))) + b.stmt( + serialize_set_binding(assignment, context, () => context.visit(assignment), false) + ) ]) ); } @@ -1026,7 +1028,7 @@ function serialize_bind_this(bind_this, context, node) { const bind_this_id = /** @type {import('estree').Expression} */ (context.visit(bind_this)); const ids = Array.from(each_ids.values()).map((id) => b.id('$$value_' + id[0])); const assignment = b.assignment('=', bind_this, b.id('$$value')); - const update = serialize_set_binding(assignment, context, () => context.visit(assignment)); + const update = serialize_set_binding(assignment, context, () => context.visit(assignment), false); for (const [binding, [, , expression]] of each_ids) { // reset expressions to what they were before @@ -2400,7 +2402,7 @@ export const template_visitors = { if (assignment.left.type !== 'Identifier' && assignment.left.type !== 'MemberExpression') { // serialize_set_binding turns other patterns into IIFEs and separates the assignments // into separate expressions, at which point this is called again with an identifier or member expression - return serialize_set_binding(assignment, context, () => assignment); + return serialize_set_binding(assignment, context, () => assignment, false); } const left = object(assignment.left); const value = get_assignment_value(assignment, context); @@ -2438,7 +2440,7 @@ export const template_visitors = { : b.id(node.index); const item = each_node_meta.item; const binding = /** @type {import('#compiler').Binding} */ (context.state.scope.get(item.name)); - binding.expression = (id) => { + binding.expression = (/** @type {import("estree").Identifier} */ id) => { const item_with_loc = with_loc(item, id); return b.call('$.unwrap', item_with_loc); }; @@ -2762,6 +2764,7 @@ export const template_visitors = { assignment, context, () => /** @type {import('estree').Expression} */ (visit(assignment)), + false, { skip_proxy_and_freeze: true } diff --git a/packages/svelte/tests/runtime-runes/samples/state-update/_config.js b/packages/svelte/tests/runtime-runes/samples/state-update/_config.js new file mode 100644 index 000000000..9d183557f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/state-update/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + test({ assert, logs }) { + assert.deepEqual(logs, [1, 1, 1, 1]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/state-update/main.svelte b/packages/svelte/tests/runtime-runes/samples/state-update/main.svelte new file mode 100644 index 000000000..011a3f693 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/state-update/main.svelte @@ -0,0 +1,9 @@ +