diff --git a/.changeset/friendly-months-prove.md b/.changeset/friendly-months-prove.md new file mode 100644 index 0000000000..bdd061db89 --- /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/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index 19a4342b5e..41ed277898 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 798d1bcdac..9fcc33287c 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 db5fe5252e..2fc3a8ed80 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 6384a6d4e7..820658d950 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,6 +1,6 @@ /** @import { AwaitExpression, Expression } from 'estree' */ /** @import { Context } from '../types' */ -import * as b from '../../../../utils/builders.js'; +import { save } from '../../../../utils/ast.js'; /** * @param {AwaitExpression} node @@ -10,7 +10,30 @@ 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); + } + + // 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/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js index d988cd98fb..541921befb 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))); +} 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 0000000000..044c6aeb9a --- /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: ` +