From 31a239833e4632a9c9ba73a2b9df10c9f8b912cf Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Sun, 16 Nov 2025 22:51:31 +0100 Subject: [PATCH 1/3] fix: parallelize async `@const`s in the template This fixes #17075 by solving the TODO of #17038 to add out of order rendering for async `@const` declarations in the template. It's implemented by a new field on the component state which is set as soon as we come across an async const. All async const declarations and those after it will be added to that field, and the existing blockers mechanism is then used to line up the async work correctly. After processing a fragment a `run` command is created from the collected consts. --- .changeset/social-taxis-tell.md | 5 ++ .changeset/stale-items-know.md | 5 ++ .../compiler/phases/1-parse/utils/create.js | 3 +- .../2-analyze/visitors/AwaitExpression.js | 4 -- .../phases/3-transform/client/types.d.ts | 5 ++ .../3-transform/client/visitors/ConstTag.js | 64 +++++++++++++++---- .../3-transform/client/visitors/Fragment.js | 19 ++---- .../client/visitors/SnippetBlock.js | 10 +-- .../client/visitors/SvelteBoundary.js | 8 ++- .../client/visitors/shared/fragment.js | 2 +- .../phases/3-transform/server/types.d.ts | 13 +++- .../3-transform/server/visitors/ConstTag.js | 26 +++++++- .../3-transform/server/visitors/EachBlock.js | 8 +-- .../3-transform/server/visitors/Fragment.js | 9 ++- .../3-transform/server/visitors/IfBlock.js | 6 +- .../server/visitors/SnippetBlock.js | 5 -- .../server/visitors/SvelteBoundary.js | 11 +--- .../server/visitors/shared/component.js | 7 +- .../svelte/src/compiler/types/template.d.ts | 2 - .../samples/async-const/_config.js | 5 +- .../samples/async-const/main.svelte | 10 +-- 21 files changed, 141 insertions(+), 86 deletions(-) create mode 100644 .changeset/social-taxis-tell.md create mode 100644 .changeset/stale-items-know.md diff --git a/.changeset/social-taxis-tell.md b/.changeset/social-taxis-tell.md new file mode 100644 index 000000000000..ea23a01def54 --- /dev/null +++ b/.changeset/social-taxis-tell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure async `@const` in boundary hydrates correctly diff --git a/.changeset/stale-items-know.md b/.changeset/stale-items-know.md new file mode 100644 index 000000000000..60fa85f595b5 --- /dev/null +++ b/.changeset/stale-items-know.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: parallelize async `@const`s in the template diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/create.js b/packages/svelte/src/compiler/phases/1-parse/utils/create.js index 2fba918f20ee..6030f1bd7bff 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/create.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/create.js @@ -10,8 +10,7 @@ export function create_fragment(transparent = false) { nodes: [], metadata: { transparent, - dynamic: false, - has_await: false + dynamic: false } }; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index 22a89db76e71..545bc3be2790 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -26,10 +26,6 @@ export function AwaitExpression(node, context) { if (context.state.expression) { context.state.expression.has_await = true; - if (context.state.fragment && context.path.some((node) => node.type === 'ConstTag')) { - context.state.fragment.metadata.has_await = true; - } - suspend = true; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index 410ad120d7e1..d64b1d41265c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -51,6 +51,11 @@ export interface ComponentClientTransformState extends ClientTransformState { readonly after_update: Statement[]; /** Transformed `{@const }` declarations */ readonly consts: Statement[]; + /** Transformed async `{@const }` declarations (if any) and those coming after them */ + async_consts?: { + id: Identifier; + thunks: Expression[]; + }; /** Transformed `let:` directives */ readonly let_directives: Statement[]; /** Memoized expressions */ diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js index 81dd9e07edd5..f3d7a3549c07 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js @@ -24,15 +24,15 @@ export function ConstTag(node, context) { expression = b.call('$.tag', expression, b.literal(declaration.id.name)); } - context.state.consts.push(b.const(declaration.id, expression)); - context.state.transform[declaration.id.name] = { read: get_value }; - // we need to eagerly evaluate the expression in order to hit any - // 'Cannot access x before initialization' errors - if (dev) { - context.state.consts.push(b.stmt(b.call('$.get', declaration.id))); - } + add_const_declaration( + context.state, + declaration.id, + expression, + node.metadata.expression.has_await, + context.state.scope.get_bindings(declaration) + ); } else { const identifiers = extract_identifiers(declaration.id); const tmp = b.id(context.state.scope.generate('computed_const')); @@ -69,13 +69,13 @@ export function ConstTag(node, context) { expression = b.call('$.tag', expression, b.literal('[@const]')); } - context.state.consts.push(b.const(tmp, expression)); - - // we need to eagerly evaluate the expression in order to hit any - // 'Cannot access x before initialization' errors - if (dev) { - context.state.consts.push(b.stmt(b.call('$.get', tmp))); - } + add_const_declaration( + context.state, + tmp, + expression, + node.metadata.expression.has_await, + context.state.scope.get_bindings(declaration) + ); for (const node of identifiers) { context.state.transform[node.name] = { @@ -84,3 +84,39 @@ export function ConstTag(node, context) { } } } + +/** + * @param {ComponentContext['state']} state + * @param {import('estree').Identifier} id + * @param {import('estree').Expression} expression + * @param {boolean} has_await + * @param {import('#compiler').Binding[]} bindings + */ +function add_const_declaration(state, id, expression, has_await, bindings) { + // we need to eagerly evaluate the expression in order to hit any + // 'Cannot access x before initialization' errors + const after = dev ? [b.stmt(b.call('$.get', id))] : []; + + if (has_await || state.async_consts) { + const run = (state.async_consts ??= { + id: b.id(state.scope.generate('promises')), + thunks: [] + }); + + state.consts.push(b.let(id)); + + const assignment = b.assignment('=', id, expression); + const body = after.length === 0 ? assignment : b.block([b.stmt(assignment), ...after]); + + run.thunks.push(b.thunk(body, has_await)); + + const blocker = b.member(run.id, b.literal(run.thunks.length - 1), true); + + for (const binding of bindings) { + binding.blocker = blocker; + } + } else { + state.consts.push(b.const(id, expression)); + state.consts.push(...after); + } +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index 8d6a2fac8825..ff2436779b92 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -48,8 +48,6 @@ export function Fragment(node, context) { const is_single_child_not_needing_template = trimmed.length === 1 && (trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement'); - const has_await = context.state.init !== null && (node.metadata.has_await || false); - const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent /** @type {Statement[]} */ @@ -72,7 +70,8 @@ export function Fragment(node, context) { metadata: { namespace, bound_contenteditable: context.state.metadata.bound_contenteditable - } + }, + async_consts: undefined }; for (const node of hoisted) { @@ -153,8 +152,8 @@ export function Fragment(node, context) { body.push(...state.let_directives, ...state.consts); - if (has_await) { - body.push(b.if(b.call('$.aborted'), b.return())); + if (state.async_consts && state.async_consts.thunks.length > 0) { + body.push(b.var(state.async_consts.id, b.call('$.run', b.array(state.async_consts.thunks)))); } if (is_text_first) { @@ -177,13 +176,5 @@ export function Fragment(node, context) { body.push(close); } - if (has_await) { - return b.block([ - b.stmt( - b.call('$.async_body', b.id('$$anchor'), b.arrow([b.id('$$anchor')], b.block(body), true)) - ) - ]); - } else { - return b.block(body); - } + return b.block(body); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js index 895522d47ab2..1af737f05b35 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js @@ -14,8 +14,6 @@ export function SnippetBlock(node, context) { // TODO hoist where possible /** @type {(Identifier | AssignmentPattern)[]} */ const args = [b.id('$$anchor')]; - const has_await = node.body.metadata.has_await || false; - /** @type {BlockStatement} */ let body; @@ -78,12 +76,8 @@ export function SnippetBlock(node, context) { // in dev we use a FunctionExpression (not arrow function) so we can use `arguments` let snippet = dev - ? b.call( - '$.wrap_snippet', - b.id(context.state.analysis.name), - b.function(null, args, body, has_await) - ) - : b.arrow(args, body, has_await); + ? b.call('$.wrap_snippet', b.id(context.state.analysis.name), b.function(null, args, body)) + : b.arrow(args, body); const declaration = b.const(node.expression, snippet); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js index 49c89bc438e0..9b7326cf4398 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js @@ -101,7 +101,13 @@ export function SvelteBoundary(node, context) { nodes.push(child); } - const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes })); + const block = /** @type {BlockStatement} */ ( + context.visit( + { ...node.fragment, nodes }, + // Since we're creating a new fragment the reference in scopes can't match, so we gotta attach the right scope manually + { ...context.state, scope: context.state.scopes.get(node.fragment) ?? context.state.scope } + ) + ); if (!context.state.options.experimental.async) { block.body.unshift(...const_tags); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js index c7f843af4822..67982b6150b7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js @@ -105,7 +105,7 @@ export function process_children(nodes, initial, is_element, context) { is_element && // In case it's wrapped in async the async logic will want to skip sibling nodes up until the end, hence we cannot make this controlled // TODO switch this around and instead optimize for elements with a single block child and not require extra comments (neither for async nor normally) - !(node.body.metadata.has_await || node.metadata.expression.is_async()) + !node.metadata.expression.is_async() ) { node.metadata.is_controlled = true; } else { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts index adde7480cbd1..e7a72fb8ad41 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts @@ -1,4 +1,10 @@ -import type { Expression, Statement, ModuleDeclaration, LabeledStatement } from 'estree'; +import type { + Expression, + Statement, + ModuleDeclaration, + LabeledStatement, + Identifier +} from 'estree'; import type { AST, Namespace, ValidatedCompileOptions } from '#compiler'; import type { TransformState } from '../types.js'; import type { ComponentAnalysis } from '../../types.js'; @@ -21,6 +27,11 @@ export interface ComponentServerTransformState extends ServerTransformState { readonly namespace: Namespace; readonly preserve_whitespace: boolean; readonly skip_hydration_boundaries: boolean; + /** Transformed async `{@const }` declarations (if any) and those coming after them */ + async_consts?: { + id: Identifier; + thunks: Expression[]; + }; } export type Context = import('zimmerframe').Context; diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ConstTag.js index a8e4e575cc68..c549d1d00945 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ConstTag.js @@ -2,6 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import * as b from '#compiler/builders'; +import { extract_identifiers } from '../../../../utils/ast.js'; /** * @param {AST.ConstTag} node @@ -11,6 +12,29 @@ export function ConstTag(node, context) { const declaration = node.declaration.declarations[0]; const id = /** @type {Pattern} */ (context.visit(declaration.id)); const init = /** @type {Expression} */ (context.visit(declaration.init)); + const has_await = node.metadata.expression.has_await; - context.state.init.push(b.const(id, init)); + if (has_await || context.state.async_consts) { + const run = (context.state.async_consts ??= { + id: b.id(context.state.scope.generate('promises')), + thunks: [] + }); + + const identifiers = extract_identifiers(declaration.id); + const bindings = context.state.scope.get_bindings(declaration); + + for (const identifier of identifiers) { + context.state.init.push(b.let(identifier.name)); + } + + const assignment = b.assignment('=', id, init); + run.thunks.push(b.thunk(b.block([b.stmt(assignment)]), has_await)); + + const blocker = b.member(run.id, b.literal(run.thunks.length - 1), true); + for (const binding of bindings) { + binding.blocker = blocker; + } + } else { + context.state.init.push(b.const(id, init)); + } } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js index f53a3903c2b9..3c0a8c167696 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js @@ -34,11 +34,7 @@ export function EachBlock(node, context) { const new_body = /** @type {BlockStatement} */ (context.visit(node.body)).body; - if (node.body) - each.push( - // TODO get rid of fragment.has_await - ...(node.body.metadata.has_await ? [create_async_block(b.block(new_body))] : new_body) - ); + if (node.body) each.push(...new_body); const for_loop = b.for( b.declaration('let', [ @@ -61,7 +57,7 @@ export function EachBlock(node, context) { b.if( b.binary('!==', b.member(array_id, 'length'), b.literal(0)), b.block([open, for_loop]), - node.fallback.metadata.has_await ? create_async_block(fallback) : fallback + fallback ) ); } else { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js index a1d25980c438..ef5bd985ae5d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js @@ -28,7 +28,8 @@ export function Fragment(node, context) { init: [], template: [], namespace, - skip_hydration_boundaries: is_standalone + skip_hydration_boundaries: is_standalone, + async_consts: undefined }; for (const node of hoisted) { @@ -42,5 +43,11 @@ export function Fragment(node, context) { process_children(trimmed, { ...context, state }); + if (state.async_consts && state.async_consts.thunks.length > 0) { + state.init.push( + b.var(state.async_consts.id, b.call('$$renderer.run', b.array(state.async_consts.thunks))) + ); + } + return b.block([...state.init, ...build_template(state.template)]); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js index ca614a93e233..e8418343be9b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js @@ -25,11 +25,7 @@ export function IfBlock(node, context) { const is_async = node.metadata.expression.is_async(); - const has_await = - node.metadata.expression.has_await || - // TODO get rid of this stuff - node.consequent.metadata.has_await || - node.alternate?.metadata.has_await; + const has_await = node.metadata.expression.has_await; if (is_async || has_await) { statement = create_async_block( diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js index 5fc865ec586a..7ae2a8e03793 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js @@ -3,7 +3,6 @@ /** @import { ComponentContext } from '../types.js' */ import { dev } from '../../../../state.js'; import * as b from '#compiler/builders'; -import { create_async_block } from './shared/utils.js'; /** * @param {AST.SnippetBlock} node @@ -16,10 +15,6 @@ export function SnippetBlock(node, context) { /** @type {BlockStatement} */ (context.visit(node.body)) ); - if (node.body.metadata.has_await) { - fn.body = b.block([create_async_block(fn.body)]); - } - // @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone fn.___snippet = true; diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js index 45f1b5aad2b3..7023899a9b1e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js @@ -7,8 +7,7 @@ import { block_open, block_open_else, build_attribute_value, - build_template, - create_async_block + build_template } from './shared/utils.js'; /** @@ -43,9 +42,7 @@ export function SvelteBoundary(node, context) { ); const pending = b.call(callee, b.id('$$renderer')); const block = /** @type {BlockStatement} */ (context.visit(node.fragment)); - const statement = node.fragment.metadata.has_await - ? create_async_block(b.block([block])) - : block; + const statement = block; context.state.template.push( b.if( callee, @@ -70,9 +67,7 @@ export function SvelteBoundary(node, context) { } } else { const block = /** @type {BlockStatement} */ (context.visit(node.fragment)); - const statement = node.fragment.metadata.has_await - ? create_async_block(b.block([block])) - : block; + const statement = block; context.state.template.push(block_open, statement, block_close); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js index 2e7b4a186c0c..6f2ff38bc1c9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js @@ -244,12 +244,7 @@ export function build_inline_component(node, expression, context) { params.push(pattern); } - const slot_fn = b.arrow( - params, - node.fragment.metadata.has_await - ? b.block([create_async_block(b.block(block.body))]) - : b.block(block.body) - ); + const slot_fn = b.arrow(params, b.block(block.body)); if (slot_name === 'default' && !has_children_prop) { if ( diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 3960c95c8f71..fd664f107c0e 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -48,8 +48,6 @@ export namespace AST { * Whether or not we need to traverse into the fragment during mount/hydrate */ dynamic: boolean; - /** @deprecated we should get rid of this in favour of the `$$renderer.run` mechanism */ - has_await: boolean; }; } diff --git a/packages/svelte/tests/runtime-runes/samples/async-const/_config.js b/packages/svelte/tests/runtime-runes/samples/async-const/_config.js index 8aeca875f395..c3e74e886a5e 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-const/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-const/_config.js @@ -2,11 +2,12 @@ import { tick } from 'svelte'; import { test } from '../../test'; export default test({ - html: `

Loading...

`, + mode: ['async-server', 'client', 'hydrate'], + ssrHtml: `

Hello, world!

5 01234 5 sync 6 5 0`, async test({ assert, target }) { await tick(); - assert.htmlEqual(target.innerHTML, `

Hello, world!

5 01234`); + assert.htmlEqual(target.innerHTML, `

Hello, world!

5 01234 5 sync 6 5 0`); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte index 7410ff6a6fd0..b7e00803c55a 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte @@ -3,17 +3,16 @@ + {@const sync = 'sync'} {@const number = await Promise.resolve(5)} - - {#snippet pending()} -

Loading...

- {/snippet} + {@const after_async = number + 1} + {@const { length, 0: first } = await '01234'} {#snippet greet()} {@const greeting = await `Hello, ${name}!`}

{greeting}

{number} - {#if number > 4} + {#if number > 4 && after_async && greeting} {@const length = await number} {#each { length }, index} {@const i = await index} @@ -23,4 +22,5 @@ {/snippet} {@render greet()} + {number} {sync} {after_async} {length} {first}
From b73a22849f38724889b811f020b2115b661978f9 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Sun, 16 Nov 2025 23:01:59 +0100 Subject: [PATCH 2/3] fix --- .../client/visitors/SvelteBoundary.js | 6 ++- .../_expected/client/index.svelte.js | 23 +++++---- .../_expected/server/index.svelte.js | 50 ++++++++++++------- 3 files changed, 50 insertions(+), 29 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js index 9b7326cf4398..d64fcda2e8ed 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js @@ -48,7 +48,11 @@ export function SvelteBoundary(node, context) { if (child.type === 'ConstTag') { has_const = true; if (!context.state.options.experimental.async) { - context.visit(child, { ...context.state, consts: const_tags }); + context.visit(child, { + ...context.state, + consts: const_tags, + scope: context.state.scopes.get(node.fragment) ?? context.state.scope + }); } } } diff --git a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js index e4df43c6c26b..7d1fe4ec67aa 100644 --- a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js @@ -25,20 +25,23 @@ export default function Async_in_derived($$anchor, $$props) { { var consequent = ($$anchor) => { - $.async_body($$anchor, async ($$anchor) => { - const yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))(); - const yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))(); + let yes1; + let yes2; + let no1; + let no2; - const no1 = $.derived(() => (async () => { - return await 1; - })()); + var promises = $.run([ + async () => yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))(), + async () => yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))(), - const no2 = $.derived(() => (async () => { + () => no1 = $.derived(() => (async () => { return await 1; - })()); + })()), - if ($.aborted()) return; - }); + () => no2 = $.derived(() => (async () => { + return await 1; + })()) + ]); }; $.if(node, ($$render) => { diff --git a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js index bece6402c665..1fd184fa79e4 100644 --- a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js @@ -18,24 +18,38 @@ export default function Async_in_derived($$renderer, $$props) { } ]); - $$renderer.async_block([], async ($$renderer) => { - if (true) { - $$renderer.push(''); - - const yes1 = (await $.save(1))(); - const yes2 = foo((await $.save(1))()); - - const no1 = (async () => { - return await 1; - })(); - - const no2 = (async () => { - return await 1; - })(); - } else { - $$renderer.push(''); - } - }); + if (true) { + $$renderer.push(''); + + let yes1; + let yes2; + let no1; + let no2; + + var promises = $$renderer.run([ + async () => { + yes1 = (await $.save(1))(); + }, + + async () => { + yes2 = foo((await $.save(1))()); + }, + + () => { + no1 = (async () => { + return await 1; + })(); + }, + + () => { + no2 = (async () => { + return await 1; + })(); + } + ]); + } else { + $$renderer.push(''); + } $$renderer.push(``); }); From 7c0fda4af6039f5125213251f42a239f5676be18 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 17 Nov 2025 10:14:58 -0500 Subject: [PATCH 3/3] tweak --- .../phases/3-transform/server/visitors/SvelteBoundary.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js index 7023899a9b1e..8a30e765c230 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js @@ -42,12 +42,11 @@ export function SvelteBoundary(node, context) { ); const pending = b.call(callee, b.id('$$renderer')); const block = /** @type {BlockStatement} */ (context.visit(node.fragment)); - const statement = block; context.state.template.push( b.if( callee, b.block(build_template([block_open_else, b.stmt(pending), block_close])), - b.block(build_template([block_open, statement, block_close])) + b.block(build_template([block_open, block, block_close])) ) ); } else { @@ -67,7 +66,6 @@ export function SvelteBoundary(node, context) { } } else { const block = /** @type {BlockStatement} */ (context.visit(node.fragment)); - const statement = block; - context.state.template.push(block_open, statement, block_close); + context.state.template.push(block_open, block, block_close); } }