Skip to content

Commit 97d5cf1

Browse files
authored
chore: simplify assignments in server code (#12614)
Also fixes an uncovered bug where store `+=/-=` etc assignments were not serialized correctly on the server
1 parent 993fff0 commit 97d5cf1

File tree

9 files changed

+138
-141
lines changed

9 files changed

+138
-141
lines changed

.changeset/violet-otters-carry.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: correctly update stores when reassigning with operator other than `=`

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ export function serialize_set_binding(node, context, fallback, prefix, options)
382382
}
383383

384384
return b.call(
385-
'$.mutate_store',
385+
'$.store_mutate',
386386
serialize_get_binding(b.id(left_name), state),
387387
b.assignment(node.operator, /** @type {Pattern}} */ (visit_node(node.left)), value),
388388
b.call('$.untrack', b.id('$' + left_name))
Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,118 @@
1-
/** @import { AssignmentExpression } from 'estree' */
2-
/** @import { Context } from '../types.js' */
3-
import { serialize_set_binding } from './shared/utils.js';
1+
/** @import { AssignmentExpression, AssignmentOperator, BinaryOperator, Expression, Node, Pattern } from 'estree' */
2+
/** @import { SvelteNode } from '#compiler' */
3+
/** @import { Context, ServerTransformState } from '../types.js' */
4+
import * as b from '../../../../utils/builders.js';
5+
import { extract_paths } from '../../../../utils/ast.js';
6+
import { serialize_get_binding } from './shared/utils.js';
47

58
/**
69
* @param {AssignmentExpression} node
710
* @param {Context} context
811
*/
912
export function AssignmentExpression(node, context) {
10-
return serialize_set_binding(node, context, context.next);
13+
const parent = /** @type {Node} */ (context.path.at(-1));
14+
const is_standalone = parent.type.endsWith('Statement');
15+
16+
if (
17+
node.left.type === 'ArrayPattern' ||
18+
node.left.type === 'ObjectPattern' ||
19+
node.left.type === 'RestElement'
20+
) {
21+
const value = /** @type {Expression} */ (context.visit(node.right));
22+
const should_cache = value.type !== 'Identifier';
23+
const rhs = should_cache ? b.id('$$value') : value;
24+
25+
let changed = false;
26+
27+
const assignments = extract_paths(node.left).map((path) => {
28+
const value = path.expression?.(rhs);
29+
30+
let assignment = serialize_assignment('=', path.node, value, context);
31+
if (assignment !== null) changed = true;
32+
33+
return assignment ?? b.assignment('=', path.node, value);
34+
});
35+
36+
if (!changed) {
37+
// No change to output -> nothing to transform -> we can keep the original assignment
38+
return context.next();
39+
}
40+
41+
const sequence = b.sequence(assignments);
42+
43+
if (!is_standalone) {
44+
// this is part of an expression, we need the sequence to end with the value
45+
sequence.expressions.push(rhs);
46+
}
47+
48+
if (should_cache) {
49+
// the right hand side is a complex expression, wrap in an IIFE to cache it
50+
return b.call(b.arrow([rhs], sequence), value);
51+
}
52+
53+
return sequence;
54+
}
55+
56+
return serialize_assignment(node.operator, node.left, node.right, context) || context.next();
57+
}
58+
59+
/**
60+
* Only returns an expression if this is not a `$store` assignment, as others can be kept as-is
61+
* @param {AssignmentOperator} operator
62+
* @param {Pattern} left
63+
* @param {Expression} right
64+
* @param {import('zimmerframe').Context<SvelteNode, ServerTransformState>} context
65+
* @returns {Expression | null}
66+
*/
67+
function serialize_assignment(operator, left, right, context) {
68+
let object = left;
69+
70+
while (object.type === 'MemberExpression') {
71+
// @ts-expect-error
72+
object = object.object;
73+
}
74+
75+
if (object.type !== 'Identifier' || !is_store_name(object.name)) {
76+
return null;
77+
}
78+
79+
const name = object.name.slice(1);
80+
81+
if (!context.state.scope.get(name)) {
82+
return null;
83+
}
84+
85+
if (object === left) {
86+
let value = /** @type {Expression} */ (context.visit(right));
87+
88+
if (operator !== '=') {
89+
// turn `x += 1` into `x = x + 1`
90+
value = b.binary(
91+
/** @type {BinaryOperator} */ (operator.slice(0, -1)),
92+
serialize_get_binding(left, context.state),
93+
value
94+
);
95+
}
96+
97+
return b.call('$.store_set', b.id(name), value);
98+
}
99+
100+
return b.call(
101+
'$.store_mutate',
102+
b.assignment('??=', b.id('$$store_subs'), b.object([])),
103+
b.literal(object.name),
104+
b.id(name),
105+
b.assignment(
106+
operator,
107+
/** @type {Pattern} */ (context.visit(left)),
108+
/** @type {Expression} */ (context.visit(right))
109+
)
110+
);
111+
}
112+
113+
/**
114+
* @param {string} name
115+
*/
116+
function is_store_name(name) {
117+
return name[0] === '$' && /[A-Za-z_]/.test(name[1]);
11118
}

packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js

Lines changed: 2 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
/** @import { AssignmentExpression, AssignmentOperator, BinaryOperator, Expression, Identifier, Node, Pattern, Statement, TemplateElement } from 'estree' */
1+
/** @import { AssignmentOperator, Expression, Identifier, Node, Statement, TemplateElement } from 'estree' */
22
/** @import { Attribute, Comment, ExpressionTag, SvelteNode, Text } from '#compiler' */
33
/** @import { ComponentContext, ServerTransformState } from '../../types.js' */
4-
import { extract_paths } from '../../../../../utils/ast.js';
4+
55
import { escape_html } from '../../../../../../escaping.js';
66
import {
77
BLOCK_CLOSE,
@@ -227,134 +227,3 @@ export function serialize_get_binding(node, state) {
227227

228228
return node;
229229
}
230-
231-
/**
232-
* @param {AssignmentExpression} node
233-
* @param {import('zimmerframe').Context<SvelteNode, ServerTransformState>} context
234-
* @param {() => any} fallback
235-
* @returns {Expression}
236-
*/
237-
export function serialize_set_binding(node, context, fallback) {
238-
const { state, visit } = context;
239-
240-
if (
241-
node.left.type === 'ArrayPattern' ||
242-
node.left.type === 'ObjectPattern' ||
243-
node.left.type === 'RestElement'
244-
) {
245-
// Turn assignment into an IIFE, so that `$.set` calls etc don't produce invalid code
246-
const tmp_id = context.state.scope.generate('tmp');
247-
248-
/** @type {AssignmentExpression[]} */
249-
const original_assignments = [];
250-
251-
/** @type {Expression[]} */
252-
const assignments = [];
253-
254-
const paths = extract_paths(node.left);
255-
256-
for (const path of paths) {
257-
const value = path.expression?.(b.id(tmp_id));
258-
const assignment = b.assignment('=', path.node, value);
259-
original_assignments.push(assignment);
260-
assignments.push(serialize_set_binding(assignment, context, () => assignment));
261-
}
262-
263-
if (assignments.every((assignment, i) => assignment === original_assignments[i])) {
264-
// No change to output -> nothing to transform -> we can keep the original assignment
265-
return fallback();
266-
}
267-
268-
return b.call(
269-
b.thunk(
270-
b.block([
271-
b.const(tmp_id, /** @type {Expression} */ (visit(node.right))),
272-
b.stmt(b.sequence(assignments)),
273-
b.return(b.id(tmp_id))
274-
])
275-
)
276-
);
277-
}
278-
279-
if (node.left.type !== 'Identifier' && node.left.type !== 'MemberExpression') {
280-
throw new Error(`Unexpected assignment type ${node.left.type}`);
281-
}
282-
283-
let left = node.left;
284-
285-
while (left.type === 'MemberExpression') {
286-
// @ts-expect-error
287-
left = left.object;
288-
}
289-
290-
if (left.type !== 'Identifier') {
291-
return fallback();
292-
}
293-
294-
const is_store = is_store_name(left.name);
295-
const left_name = is_store ? left.name.slice(1) : left.name;
296-
const binding = state.scope.get(left_name);
297-
298-
if (!binding) return fallback();
299-
300-
if (binding.mutation !== null) {
301-
return binding.mutation(node, context);
302-
}
303-
304-
if (
305-
binding.kind !== 'state' &&
306-
binding.kind !== 'frozen_state' &&
307-
binding.kind !== 'prop' &&
308-
binding.kind !== 'bindable_prop' &&
309-
binding.kind !== 'each' &&
310-
binding.kind !== 'legacy_reactive' &&
311-
!is_store
312-
) {
313-
// TODO error if it's a computed (or rest prop)? or does that already happen elsewhere?
314-
return fallback();
315-
}
316-
317-
const value = get_assignment_value(node, context);
318-
if (left === node.left) {
319-
if (is_store) {
320-
return b.call('$.store_set', b.id(left_name), /** @type {Expression} */ (visit(node.right)));
321-
}
322-
return fallback();
323-
} else if (is_store) {
324-
return b.call(
325-
'$.mutate_store',
326-
b.assignment('??=', b.id('$$store_subs'), b.object([])),
327-
b.literal(left.name),
328-
b.id(left_name),
329-
b.assignment(node.operator, /** @type {Pattern} */ (visit(node.left)), value)
330-
);
331-
}
332-
return fallback();
333-
}
334-
335-
/**
336-
* @param {AssignmentExpression} node
337-
* @param {Pick<import('zimmerframe').Context<SvelteNode, ServerTransformState>, 'visit' | 'state'>} context
338-
*/
339-
function get_assignment_value(node, { state, visit }) {
340-
if (node.left.type === 'Identifier') {
341-
const operator = node.operator;
342-
return operator === '='
343-
? /** @type {Expression} */ (visit(node.right))
344-
: // turn something like x += 1 into x = x + 1
345-
b.binary(
346-
/** @type {BinaryOperator} */ (operator.slice(0, -1)),
347-
serialize_get_binding(node.left, state),
348-
/** @type {Expression} */ (visit(node.right))
349-
);
350-
}
351-
352-
return /** @type {Expression} */ (visit(node.right));
353-
}
354-
355-
/**
356-
* @param {string} name
357-
*/
358-
function is_store_name(name) {
359-
return name[0] === '$' && /[A-Za-z_]/.test(name[1]);
360-
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export {
116116
} from './reactivity/props.js';
117117
export {
118118
invalidate_store,
119-
mutate_store,
119+
store_mutate,
120120
setup_stores,
121121
store_get,
122122
store_set,

packages/svelte/src/internal/client/reactivity/store.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export function setup_stores() {
119119
* @param {V} new_value the new store value
120120
* @template V
121121
*/
122-
export function mutate_store(store, expression, new_value) {
122+
export function store_mutate(store, expression, new_value) {
123123
store.set(new_value);
124124
return expression;
125125
}

packages/svelte/src/internal/server/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ export function store_set(store, value) {
362362
* @param {Store<V>} store
363363
* @param {any} expression
364364
*/
365-
export function mutate_store(store_values, store_name, store, expression) {
365+
export function store_mutate(store_values, store_name, store, expression) {
366366
store_set(store, store_get(store_values, store_name, store));
367367
return expression;
368368
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: '<p>3</p>'
5+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script>
2+
import { writable } from 'svelte/store';
3+
4+
const count = writable(0);
5+
6+
$count += 1;
7+
$count += 1;
8+
$count += 1;
9+
</script>
10+
11+
<p>{$count}</p>

0 commit comments

Comments
 (0)