From 3937ec03bdaae8d9c097f4f015560ee4b9e32cf3 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 9 Apr 2026 20:21:47 +0200 Subject: [PATCH] fix: correctly calculate `@const` blockers (#18039) Move the calculation of blockers into the analysis phase and then only push the right thunks in the transform phase - similar to how we already do it with top level `$.run` Fixes #18024 The solution is a tiny bit brittle (not much more than the top level one we already have) and I tried to make it a bit more robust but ended up in a rabbit hole in #18032 - we can revisit that solution once all the old stuff is gone. Until then this is the most pragmatic/non-invasive change. --------- Co-authored-by: Rich Harris --- .changeset/common-flowers-listen.md | 5 ++ .../src/compiler/phases/2-analyze/types.d.ts | 8 +++ .../phases/2-analyze/visitors/ConstTag.js | 25 +++++++++ .../phases/2-analyze/visitors/Fragment.js | 2 +- .../3-transform/client/visitors/ConstTag.js | 51 +++++++------------ .../3-transform/server/visitors/ConstTag.js | 25 ++++----- .../svelte/src/compiler/types/template.d.ts | 2 + .../async-derived-const-blocker/_config.js | 9 ++++ .../async-derived-const-blocker/main.svelte | 16 ++++++ .../_expected/server/index.svelte.js | 11 +--- .../_expected/server/index.svelte.js | 28 ++++------ 11 files changed, 107 insertions(+), 75 deletions(-) create mode 100644 .changeset/common-flowers-listen.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-const-blocker/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-const-blocker/main.svelte diff --git a/.changeset/common-flowers-listen.md b/.changeset/common-flowers-listen.md new file mode 100644 index 0000000000..8d021b8054 --- /dev/null +++ b/.changeset/common-flowers-listen.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly calculate `@const` blockers diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index 9d24f9dbac..7941113a7f 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -2,6 +2,7 @@ import type { Scope } from '../scope.js'; import type { ComponentAnalysis, ReactiveStatement } from '../types.js'; import type { AST, StateField, ValidatedCompileOptions } from '#compiler'; import type { ExpressionMetadata } from '../nodes.js'; +import type { Identifier } from 'estree'; export interface AnalysisState { scope: Scope; @@ -33,6 +34,13 @@ export interface AnalysisState { * Set when we're inside a `$derived(...)` expression (but not `$derived.by(...)`) or `@const` */ derived_function_depth: number; + + /** Collected info about async `{@const }` declarations */ + async_consts?: { + id: Identifier; + /** How many `@const` declarations there are (already) in this scope */ + declaration_count: number; + }; } export type Context = import('zimmerframe').Context< diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js index 77ea654905..22ffc24c1e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js @@ -1,6 +1,7 @@ /** @import { AST } from '#compiler' */ /** @import { Context } from '../types' */ import * as e from '../../../errors.js'; +import * as b from '#compiler/builders'; import { validate_opening_tag } from './shared/utils.js'; /** @@ -42,4 +43,28 @@ export function ConstTag(node, context) { function_depth: context.state.function_depth + 1, derived_function_depth: context.state.function_depth + 1 }); + + const has_await = node.metadata.expression.has_await; + const blockers = [...node.metadata.expression.dependencies] + .map((dep) => dep.blocker) + .filter((b) => b !== null && b.object !== context.state.async_consts?.id); + + if (has_await || context.state.async_consts || blockers.length > 0) { + const run = (context.state.async_consts ??= { + id: context.state.analysis.root.unique('promises'), + declaration_count: 0 + }); + node.metadata.promises_id = run.id; + + const bindings = context.state.scope.get_bindings(declaration); + + // keep the counter in sync with the number of thunks pushed in ConstTag in transform + // TODO 6.0 once non-async and non-runes mode is gone investigate making this more robust + // via something like the approach in https://github.com/sveltejs/svelte/pull/18032 + const length = run.declaration_count++; + const blocker = b.member(run.id, b.literal(length), true); + for (const binding of bindings) { + binding.blocker = blocker; + } + } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js index 02d780dc0d..def3860175 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js @@ -6,5 +6,5 @@ * @param {Context} context */ export function Fragment(node, context) { - context.next({ ...context.state, fragment: node }); + context.next({ ...context.state, fragment: node, async_consts: undefined }); } 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 d2bd3c10dc..ac8f120d3d 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 @@ -1,7 +1,6 @@ -/** @import { Expression, Identifier, Pattern } from 'estree' */ +/** @import { Expression, Identifier, Pattern, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ -/** @import { ExpressionMetadata } from '../../../nodes.js' */ import { dev } from '../../../../state.js'; import { extract_identifiers } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; @@ -27,13 +26,7 @@ export function ConstTag(node, context) { context.state.transform[declaration.id.name] = { read: get_value }; - add_const_declaration( - context.state, - declaration.id, - expression, - node.metadata.expression, - context.state.scope.get_bindings(declaration) - ); + add_const_declaration(context.state, declaration.id, expression, node.metadata); } else { const identifiers = extract_identifiers(declaration.id); const tmp = b.id(context.state.scope.generate('computed_const')); @@ -70,13 +63,7 @@ export function ConstTag(node, context) { expression = b.call('$.tag', expression, b.literal('[@const]')); } - add_const_declaration( - context.state, - tmp, - expression, - node.metadata.expression, - context.state.scope.get_bindings(declaration) - ); + add_const_declaration(context.state, tmp, expression, node.metadata); for (const node of identifiers) { context.state.transform[node.name] = { @@ -90,42 +77,40 @@ export function ConstTag(node, context) { * @param {ComponentContext['state']} state * @param {Identifier} id * @param {Expression} expression - * @param {ExpressionMetadata} metadata - * @param {import('#compiler').Binding[]} bindings + * @param {AST.ConstTag['metadata']} metadata */ -function add_const_declaration(state, id, expression, metadata, bindings) { +function add_const_declaration(state, id, expression, metadata) { // 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))] : []; - const has_await = metadata.has_await; - const blockers = [...metadata.dependencies] + const blockers = [...metadata.expression.dependencies] .map((dep) => dep.blocker) .filter((b) => b !== null && b.object !== state.async_consts?.id); - if (has_await || state.async_consts || blockers.length > 0) { + if (metadata.promises_id) { const run = (state.async_consts ??= { - id: b.id(state.scope.generate('promises')), + id: metadata.promises_id, 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]); + /** @type {Statement | undefined} */ + let promise_stmt; if (blockers.length === 1) { - run.thunks.push(b.thunk(b.member(/** @type {Expression} */ (blockers[0]), 'promise'))); + promise_stmt = b.stmt(b.await(b.member(/** @type {Expression} */ (blockers[0]), 'promise'))); } else if (blockers.length > 0) { - run.thunks.push(b.thunk(b.call('$.wait', b.array(blockers)))); + promise_stmt = b.stmt(b.await(b.call('$.wait', b.array(blockers)))); } - 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; + // keep the number of thunks pushed in sync with ConstTag in analysis phase + const assignment = b.assignment('=', id, expression); + if (promise_stmt) { + run.thunks.push(b.thunk(b.block([promise_stmt, b.stmt(assignment)]), true)); + } else { + run.thunks.push(b.thunk(assignment, metadata.expression.has_await)); } } else { state.consts.push(b.const(id, expression)); 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 d2ff9a10b4..87d60442f6 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 @@ -1,4 +1,4 @@ -/** @import { Expression, Pattern } from 'estree' */ +/** @import { Expression, Pattern, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import * as b from '#compiler/builders'; @@ -12,36 +12,37 @@ 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; const blockers = [...node.metadata.expression.dependencies] .map((dep) => dep.blocker) .filter((b) => b !== null && b.object !== context.state.async_consts?.id); - if (has_await || context.state.async_consts || blockers.length > 0) { + if (node.metadata.promises_id) { const run = (context.state.async_consts ??= { - id: b.id(context.state.scope.generate('promises')), + id: node.metadata.promises_id, 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)); } + /** @type {Statement | undefined} */ + let promise_stmt; + if (blockers.length === 1) { - run.thunks.push(b.thunk(/** @type {Expression} */ (blockers[0]))); + promise_stmt = b.stmt(b.await(/** @type {Expression} */ (blockers[0]))); } else if (blockers.length > 0) { - run.thunks.push(b.thunk(b.call('Promise.all', b.array(blockers)))); + promise_stmt = b.stmt(b.await(b.call('Promise.all', b.array(blockers)))); } + // keep the number of thunks pushed in sync with ConstTag in analysis phase 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; + if (promise_stmt) { + run.thunks.push(b.thunk(b.block([promise_stmt, b.stmt(assignment)]), true)); + } else { + run.thunks.push(b.thunk(assignment, node.metadata.expression.has_await)); } } else { context.state.init.push(b.const(id, init)); diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 3c1e3e772c..2964e46761 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -155,6 +155,8 @@ export namespace AST { /** @internal */ metadata: { expression: ExpressionMetadata; + /** If this const tag contains an await expression, or needs to wait on other async, this is set */ + promises_id?: Identifier; }; } diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-const-blocker/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-const-blocker/_config.js new file mode 100644 index 0000000000..afa737f36c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-const-blocker/_config.js @@ -0,0 +1,9 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

data

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-const-blocker/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-const-blocker/main.svelte new file mode 100644 index 0000000000..a154859683 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-const-blocker/main.svelte @@ -0,0 +1,16 @@ + + +{#if d} + {@const {data, hasData} = d} + + {#if hasData} +

{data}

+ {:else if showFetchCta} +

Fetch now

+ {:else} +

No data

+ {/if} +{/if} diff --git a/packages/svelte/tests/snapshot/samples/async-const/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-const/_expected/server/index.svelte.js index 5f3cfa6ca6..c0cb999e85 100644 --- a/packages/svelte/tests/snapshot/samples/async-const/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-const/_expected/server/index.svelte.js @@ -7,16 +7,7 @@ export default function Async_const($$renderer) { let a; let b; - - var promises = $$renderer.run([ - async () => { - a = (await $.save(1))(); - }, - - () => { - b = a + 1; - } - ]); + var promises = $$renderer.run([async () => a = (await $.save(1))(), () => b = a + 1]); $$renderer.push(`

`); $$renderer.async([promises[1]], ($$renderer) => $$renderer.push(() => $.escape(b))); 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 3a53475944..0cebc412ef 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 @@ -28,25 +28,15 @@ export default function Async_in_derived($$renderer, $$props) { 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; - })(); - } + async () => yes1 = (await $.save(1))(), + async () => yes2 = foo((await $.save(1))()), + () => no1 = (async () => { + return await 1; + })(), + + () => no2 = (async () => { + return await 1; + })() ]); } else { $$renderer.push('');