From 965f2a0ac863f98fc3f85e1dc6664ab69fdad267 Mon Sep 17 00:00:00 2001 From: Conduitry Date: Fri, 13 Mar 2026 14:38:36 -0400 Subject: [PATCH] fix: handle async RHS in assignment_value_stale (#17925) Fixes #17924. This also DRYs stuff a bit by making `operator` an argument to the runtime helper function, which means we only need two variants of it: regular and async. It also makes it so that `=` assignments don't use the getter, because they don't need to be done lazily. I've added `skip_no_async` to the new test, but I'm not entirely clear on why it was failing the TestNoAsync run to begin with. ### Before submitting the PR, please make sure you do the following - [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs - [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`. - [x] This message body should clearly illustrate what problems it solves. - [x] Ideally, include a test that fails without this PR but passes with it. - [x] If this PR changes code within `packages/svelte/src`, add a changeset (`npx changeset`). ### Tests and linting - [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` --------- Co-authored-by: Simon Holthausen --- .changeset/purple-boats-hear.md | 5 ++ .../client/visitors/AssignmentExpression.js | 44 ++++++-------- .../svelte/src/internal/client/dev/assign.js | 60 ++++++++----------- packages/svelte/src/internal/client/index.js | 2 +- .../_config.js | 25 ++++++++ .../main.svelte | 18 ++++++ 6 files changed, 92 insertions(+), 62 deletions(-) create mode 100644 .changeset/purple-boats-hear.md create mode 100644 packages/svelte/tests/runtime-runes/samples/assignment-value-stale-lazy-rhs-async/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/assignment-value-stale-lazy-rhs-async/main.svelte diff --git a/.changeset/purple-boats-hear.md b/.changeset/purple-boats-hear.md new file mode 100644 index 0000000000..785edbb1aa --- /dev/null +++ b/.changeset/purple-boats-hear.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: handle async RHS in `assignment_value_stale` diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 5282f1ed64..e66e3408e2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -5,7 +5,8 @@ import * as b from '#compiler/builders'; import { build_assignment_value, get_attribute_expression, - is_event_attribute + is_event_attribute, + is_expression_async } from '../../../../utils/ast.js'; import { dev, locate_node } from '../../../../state.js'; import { build_getter, should_proxy } from '../utils.js'; @@ -36,14 +37,6 @@ function is_non_coercive_operator(operator) { return ['=', '||=', '&&=', '??='].includes(operator); } -/** @type {Record} */ -const callees = { - '=': '$.assign', - '&&=': '$.assign_and', - '||=': '$.assign_or', - '??=': '$.assign_nullish' -}; - /** * @param {AssignmentOperator} operator * @param {Pattern} left @@ -179,7 +172,7 @@ function build_assignment(operator, left, right, context) { // in cases like `(object.items ??= []).push(value)`, we may need to warn // if the value gets proxified, since the proxy _isn't_ the thing that // will be pushed to. we do this by transforming it to something like - // `$.assign_nullish(object, 'items', () => [])` + // `$.assign(object, 'items', '??=', () => [])` let should_transform = dev && path.at(-1) !== 'ExpressionStatement' && @@ -225,22 +218,23 @@ function build_assignment(operator, left, right, context) { } if (left.type === 'MemberExpression' && should_transform) { - const callee = callees[operator]; - return /** @type {Expression} */ ( - context.visit( - b.call( - callee, - /** @type {Expression} */ (left.object), - /** @type {Expression} */ ( - left.computed - ? left.property - : b.literal(/** @type {Identifier} */ (left.property).name) - ), - b.arrow([], right), - b.literal(locate_node(left)) - ) - ) + const needs_lazy_getter = operator !== '='; + const needs_async = needs_lazy_getter && is_expression_async(right); + /** @type {Expression} */ + let e = b.call( + needs_async ? '$.assign_async' : '$.assign', + /** @type {Expression} */ (left.object), + /** @type {Expression} */ ( + left.computed ? left.property : b.literal(/** @type {Identifier} */ (left.property).name) + ), + b.literal(operator), + needs_lazy_getter ? b.arrow([], right, needs_async) : right, + b.literal(locate_node(left)) ); + if (needs_async) { + e = b.await(e); + } + return /** @type {Expression} */ (context.visit(e)); } return null; diff --git a/packages/svelte/src/internal/client/dev/assign.js b/packages/svelte/src/internal/client/dev/assign.js index 1cda7044b5..e328543b32 100644 --- a/packages/svelte/src/internal/client/dev/assign.js +++ b/packages/svelte/src/internal/client/dev/assign.js @@ -21,12 +21,21 @@ function compare(a, b, property, location) { /** * @param {any} object * @param {string} property - * @param {() => any} rhs_getter + * @param {string} operator + * @param {any} rhs * @param {string} location */ -export function assign(object, property, rhs_getter, location) { +export function assign(object, property, operator, rhs, location) { return compare( - (object[property] = rhs_getter()), + operator === '=' + ? (object[property] = rhs) + : operator === '&&=' + ? (object[property] &&= rhs()) + : operator === '||=' + ? (object[property] ||= rhs()) + : operator === '??=' + ? (object[property] ??= rhs()) + : null, untrack(() => object[property]), property, location @@ -36,42 +45,21 @@ export function assign(object, property, rhs_getter, location) { /** * @param {any} object * @param {string} property - * @param {() => any} rhs_getter + * @param {string} operator + * @param {any} rhs * @param {string} location */ -export function assign_and(object, property, rhs_getter, location) { +export async function assign_async(object, property, operator, rhs, location) { return compare( - (object[property] &&= rhs_getter()), - untrack(() => object[property]), - property, - location - ); -} - -/** - * @param {any} object - * @param {string} property - * @param {() => any} rhs_getter - * @param {string} location - */ -export function assign_or(object, property, rhs_getter, location) { - return compare( - (object[property] ||= rhs_getter()), - untrack(() => object[property]), - property, - location - ); -} - -/** - * @param {any} object - * @param {string} property - * @param {() => any} rhs_getter - * @param {string} location - */ -export function assign_nullish(object, property, rhs_getter, location) { - return compare( - (object[property] ??= rhs_getter()), + operator === '=' + ? (object[property] = await rhs) + : operator === '&&=' + ? (object[property] &&= await rhs()) + : operator === '||=' + ? (object[property] ||= await rhs()) + : operator === '??=' + ? (object[property] ??= await rhs()) + : null, untrack(() => object[property]), property, location diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index af8df2e32c..988998d067 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -1,7 +1,7 @@ export { createAttachmentKey as attachment } from '../../attachments/index.js'; export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js'; export { push, pop, add_svelte_meta } from './context.js'; -export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js'; +export { assign, assign_async } from './dev/assign.js'; export { cleanup_styles } from './dev/css.js'; export { add_locations } from './dev/elements.js'; export { hmr } from './dev/hmr.js'; diff --git a/packages/svelte/tests/runtime-runes/samples/assignment-value-stale-lazy-rhs-async/_config.js b/packages/svelte/tests/runtime-runes/samples/assignment-value-stale-lazy-rhs-async/_config.js new file mode 100644 index 0000000000..9ad21cffb0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/assignment-value-stale-lazy-rhs-async/_config.js @@ -0,0 +1,25 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + async test({ assert, target }) { + const button = /** @type {HTMLElement} */ (target.querySelector('button')); + await tick(); + assert.htmlEqual(target.innerHTML, `

count1: 0, count2: 0

`); + + button.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `

count1: 1, count2: 1

`); + + // additional tick necessary in legacy mode because it's using Promise.resolve() which finishes before the await in the component, + // causing the cache to not be set yet, which would result in count2 becoming 2 + await tick(); + + button.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `

count1: 2, count2: 1

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/assignment-value-stale-lazy-rhs-async/main.svelte b/packages/svelte/tests/runtime-runes/samples/assignment-value-stale-lazy-rhs-async/main.svelte new file mode 100644 index 0000000000..be4d0ad2f4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/assignment-value-stale-lazy-rhs-async/main.svelte @@ -0,0 +1,18 @@ + + + +

count1: {count1}, count2: {count2}