From b7db8b36673dd848fc4dfe1a3c61479a8e1e29f8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 17 Sep 2025 21:45:17 -0400 Subject: [PATCH 1/2] DRY out --- .../src/compiler/phases/3-transform/client/utils.js | 4 ++-- .../phases/3-transform/client/visitors/AwaitExpression.js | 3 ++- .../3-transform/client/visitors/VariableDeclaration.js | 6 +++--- .../phases/3-transform/server/visitors/AwaitExpression.js | 3 ++- packages/svelte/src/compiler/utils/ast.js | 8 ++++++++ 5 files changed, 17 insertions(+), 7 deletions(-) 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 19a4342b5eb1..41ed277898b8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -4,7 +4,7 @@ /** @import { Analysis } from '../../types.js' */ /** @import { Scope } from '../../scope.js' */ import * as b from '#compiler/builders'; -import { is_simple_expression } from '../../../utils/ast.js'; +import { is_simple_expression, save } from '../../../utils/ast.js'; import { PROPS_IS_LAZY_INITIAL, PROPS_IS_IMMUTABLE, @@ -296,7 +296,7 @@ export function create_derived(state, expression, async = false) { const thunk = b.thunk(expression, async); if (async) { - return b.call(b.await(b.call('$.save', b.call('$.async_derived', thunk)))); + return save(b.call('$.async_derived', thunk)); } else { return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', thunk); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js index 798d1bcdac7f..9fcc33287c49 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js @@ -1,6 +1,7 @@ /** @import { AwaitExpression, Expression } from 'estree' */ /** @import { Context } from '../types' */ import { dev, is_ignored } from '../../../../state.js'; +import { save } from '../../../../utils/ast.js'; import * as b from '../../../../utils/builders.js'; /** @@ -11,7 +12,7 @@ export function AwaitExpression(node, context) { const argument = /** @type {Expression} */ (context.visit(node.argument)); if (context.state.analysis.pickled_awaits.has(node)) { - return b.call(b.await(b.call('$.save', argument))); + return save(argument); } // in dev, note which values are read inside a reactive expression, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index db5fe5252e97..2fc3a8ed80e2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -2,7 +2,7 @@ /** @import { Binding } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import { dev, is_ignored, locate_node } from '../../../../state.js'; -import { extract_paths } from '../../../../utils/ast.js'; +import { extract_paths, save } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import * as assert from '../../../../utils/assert.js'; import { get_rune } from '../../../scope.js'; @@ -212,7 +212,7 @@ export function VariableDeclaration(node, context) { location ? b.literal(location) : undefined ); - call = b.call(b.await(b.call('$.save', call))); + call = save(call); if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name)); declarations.push(b.declarator(declarator.id, call)); @@ -243,7 +243,7 @@ export function VariableDeclaration(node, context) { b.thunk(expression, true), location ? b.literal(location) : undefined ); - call = b.call(b.await(b.call('$.save', call))); + call = save(call); } if (dev) { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js index 6384a6d4e704..58ecd15b33e1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js @@ -1,5 +1,6 @@ /** @import { AwaitExpression, Expression } from 'estree' */ /** @import { Context } from '../types' */ +import { save } from '../../../../utils/ast.js'; import * as b from '../../../../utils/builders.js'; /** @@ -10,7 +11,7 @@ export function AwaitExpression(node, context) { const argument = /** @type {Expression} */ (context.visit(node.argument)); if (context.state.analysis.pickled_awaits.has(node)) { - return b.call(b.await(b.call('$.save', argument))); + return save(argument); } return argument === node.argument ? node : { ...node, argument }; diff --git a/packages/svelte/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js index d988cd98fb7e..541921befbce 100644 --- a/packages/svelte/src/compiler/utils/ast.js +++ b/packages/svelte/src/compiler/utils/ast.js @@ -625,3 +625,11 @@ export function has_await(expression) { return has_await; } + +/** + * Turns `await ...` to `(await $.save(...))()` + * @param {ESTree.Expression} expression + */ +export function save(expression) { + return b.call(b.await(b.call('$.save', expression))); +} From 9309a1ab17f38c43bf31efa31ee78452f022fae0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 17 Sep 2025 22:03:49 -0400 Subject: [PATCH 2/2] fix: preserve SSR context when block expressions contain `await` --- .changeset/friendly-months-prove.md | 5 ++++ .../server/visitors/AwaitExpression.js | 24 ++++++++++++++++++- .../samples/async-push-element-dev/_config.js | 15 ++++++++++++ .../async-push-element-dev/main.svelte | 3 +++ .../_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- 8 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 .changeset/friendly-months-prove.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-push-element-dev/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-push-element-dev/main.svelte diff --git a/.changeset/friendly-months-prove.md b/.changeset/friendly-months-prove.md new file mode 100644 index 000000000000..bdd061db8919 --- /dev/null +++ b/.changeset/friendly-months-prove.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: preserve SSR context when block expressions contain `await` diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js index 58ecd15b33e1..820658d950e3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js @@ -1,7 +1,6 @@ /** @import { AwaitExpression, Expression } from 'estree' */ /** @import { Context } from '../types' */ import { save } from '../../../../utils/ast.js'; -import * as b from '../../../../utils/builders.js'; /** * @param {AwaitExpression} node @@ -14,5 +13,28 @@ export function AwaitExpression(node, context) { return save(argument); } + // we also need to restore context after block expressions + let i = context.path.length; + while (i--) { + const parent = context.path[i]; + + if ( + parent.type === 'ArrowFunctionExpression' || + parent.type === 'FunctionExpression' || + parent.type === 'FunctionDeclaration' + ) { + break; + } + + // @ts-ignore + if (parent.metadata) { + if (parent.type !== 'ExpressionTag' && parent.type !== 'Fragment') { + return save(argument); + } + + break; + } + } + return argument === node.argument ? node : { ...node, argument }; } diff --git a/packages/svelte/tests/runtime-runes/samples/async-push-element-dev/_config.js b/packages/svelte/tests/runtime-runes/samples/async-push-element-dev/_config.js new file mode 100644 index 000000000000..044c6aeb9a2a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-push-element-dev/_config.js @@ -0,0 +1,15 @@ +import { test } from '../../test'; + +export default test({ + mode: ['async-server'], + + compileOptions: { + // include `push_element` calls, so that we can check they + // run with the correct ssr_context + dev: true + }, + + html: ` +

hello!

+ ` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-push-element-dev/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-push-element-dev/main.svelte new file mode 100644 index 000000000000..99ce81e0c01c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-push-element-dev/main.svelte @@ -0,0 +1,3 @@ +{#if await true} +

hello!

+{/if} diff --git a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js index 599ef3023395..d0422add6308 100644 --- a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js @@ -3,7 +3,7 @@ import * as $ from 'svelte/internal/server'; export default function Async_each_fallback_hoisting( $$renderer) { $$renderer.child(async ( $$renderer) => { - const each_array = $.ensure_array_like(await Promise.resolve([])); + const each_array = $.ensure_array_like((await $.save(Promise.resolve([])))()); if (each_array.length !== 0) { $$renderer.push(''); diff --git a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js index e02111567787..87daa7c39fb9 100644 --- a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js @@ -9,7 +9,7 @@ export default function Async_each_hoisting( $$renderer) { $$renderer.push(``); $$renderer.child(async ( $$renderer) => { - const each_array = $.ensure_array_like(await Promise.resolve([first, second, third])); + const each_array = $.ensure_array_like((await $.save(Promise.resolve([first, second, third])))()); for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) { let item = each_array[$$index]; diff --git a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js index 021f6b113e09..0d36c38f281c 100644 --- a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js @@ -3,7 +3,7 @@ import * as $ from 'svelte/internal/server'; export default function Async_if_alternate_hoisting( $$renderer) { $$renderer.child(async ( $$renderer) => { - if (await Promise.resolve(false)) { + if ((await $.save(Promise.resolve(false)))()) { $$renderer.push(''); $$renderer.push(async () => $.escape(await Promise.reject('no no no'))); } else { diff --git a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js index 41c919d37fe7..25fdfb7e3c43 100644 --- a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js @@ -3,7 +3,7 @@ import * as $ from 'svelte/internal/server'; export default function Async_if_hoisting( $$renderer) { $$renderer.child(async ( $$renderer) => { - if (await Promise.resolve(true)) { + if ((await $.save(Promise.resolve(true)))()) { $$renderer.push(''); $$renderer.push(async () => $.escape(await Promise.resolve('yes yes yes'))); } else {