From 3e7589360a94eca01c547b7e9cb34006b53e7f88 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Oct 2025 13:17:52 -0400 Subject: [PATCH 01/56] WIP --- .../src/compiler/phases/2-analyze/index.js | 23 +++++++++++++++- .../svelte/src/compiler/phases/types.d.ts | 27 ++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index b4c704c34d..5c653c7f2e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -543,7 +543,9 @@ export function analyze_component(root, source, options) { snippet_renderers: new Map(), snippets: new Set(), async_deriveds: new Set(), - pickled_awaits: new Set() + pickled_awaits: new Set(), + awaited_declarations: new Map(), + awaited_statements: new Map() }; if (!runes) { @@ -687,6 +689,25 @@ export function analyze_component(root, source, options) { e.legacy_rest_props_invalid(rest_props_refs[0].node); } + if (instance.has_await) { + let awaiting = false; + let i = 0; + + for (const node of instance.ast.body) { + const has_await = has_await_expression(node); + awaiting ||= has_await; + + if (!awaiting) continue; + + const id = b.id(`$$${i++}`); + + analysis.awaited_statements.set(node, { + id, + has_await + }); + } + } + for (const { ast, scope, scopes } of [module, instance, template]) { /** @type {AnalysisState} */ const state = { diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index 4e287fd199..79fa4ab88e 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -5,7 +5,10 @@ import type { ClassBody, Identifier, LabeledStatement, - Program + ModuleDeclaration, + Pattern, + Program, + Statement } from 'estree'; import type { Scope, ScopeRoot } from './scope.js'; @@ -108,6 +111,28 @@ export interface ComponentAnalysis extends Analysis { * Every snippet that is declared locally */ snippets: Set; + /** + * A lookup of awaited declarations. If you have something this in ` + +{#if condition} +

yep

+{:else} +

nope

+{/if} From 2cf77396b9a67e7e29fc5a951136e1fba7f00d98 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 21:55:24 -0400 Subject: [PATCH 35/56] WIP --- .../phases/3-transform/server/visitors/EachBlock.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 2d5b0c8931..d1aa27ea25 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,7 +34,13 @@ export function EachBlock(node, context) { const new_body = /** @type {BlockStatement} */ (context.visit(node.body)).body; - each.push(...(node.body.metadata.has_await ? [create_async_block(b.block(new_body))] : new_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), b.array([]), node.body.metadata.has_await)] + : new_body) + ); const for_loop = b.for( b.declaration('let', [ From eda1dd9becbbbdc7e1014406e6ae6f17523ce588 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 22:03:55 -0400 Subject: [PATCH 36/56] WIP --- .../phases/3-transform/server/visitors/EachBlock.js | 13 ++++++++----- .../3-transform/server/visitors/shared/utils.js | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) 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 d1aa27ea25..80cbb4e690 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 @@ -37,9 +37,7 @@ export function EachBlock(node, context) { if (node.body) each.push( // TODO get rid of fragment.has_await - ...(node.body.metadata.has_await - ? [create_async_block(b.block(new_body), b.array([]), node.body.metadata.has_await)] - : new_body) + ...(node.body.metadata.has_await ? [create_async_block(b.block(new_body))] : new_body) ); const for_loop = b.for( @@ -71,8 +69,13 @@ export function EachBlock(node, context) { block.body.push(for_loop); } - if (node.metadata.expression.has_await) { - state.template.push(create_async_block(block), block_close); + const blockers = node.metadata.expression.blockers(); + + if (node.metadata.expression.has_await || blockers.elements.length > 0) { + state.template.push( + create_async_block(block, blockers, node.metadata.expression.has_await), + block_close + ); } else { state.template.push(...block.body, block_close); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js index 1e4ac69608..2fff258411 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js @@ -278,7 +278,7 @@ export function create_child_block(body, async) { * @param {ArrayExpression} blockers * @param {boolean} has_await */ -export function create_async_block(body, blockers, has_await) { +export function create_async_block(body, blockers = b.array([]), has_await = true) { return b.stmt( b.call('$$renderer.async', blockers, b.arrow([b.id('$$renderer')], body, has_await)) ); From 3f06b6cf5d012892a53f5dba2ad727469e8b77d8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 22:06:17 -0400 Subject: [PATCH 37/56] WIP --- .../phases/3-transform/server/visitors/KeyBlock.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/KeyBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/KeyBlock.js index f1dc9fa1b4..1396aa8fad 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/KeyBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/KeyBlock.js @@ -1,16 +1,22 @@ /** @import { BlockStatement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ -import { empty_comment } from './shared/utils.js'; +import { block_close, block_open, empty_comment } from './shared/utils.js'; /** * @param {AST.KeyBlock} node * @param {ComponentContext} context */ export function KeyBlock(node, context) { + const is_async = node.metadata.expression.is_async(); + + if (is_async) context.state.template.push(block_open); + context.state.template.push( empty_comment, /** @type {BlockStatement} */ (context.visit(node.fragment)), empty_comment ); + + if (is_async) context.state.template.push(block_close); } From aa547c58e5442fdb683b62f04e5dca30d47f378b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 22:36:07 -0400 Subject: [PATCH 38/56] WIP --- .../server/visitors/SvelteElement.js | 28 ++++++++++++++++--- .../svelte/src/internal/server/renderer.js | 27 ++++++++++++++---- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js index 2e61897924..145167a690 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js @@ -6,7 +6,12 @@ import { dev, locator } from '../../../../state.js'; import * as b from '#compiler/builders'; import { determine_namespace_for_children } from '../../utils.js'; import { build_element_attributes } from './shared/element.js'; -import { build_template, create_child_block, PromiseOptimiser } from './shared/utils.js'; +import { + build_template, + create_async_block, + create_child_block, + PromiseOptimiser +} from './shared/utils.js'; /** * @param {AST.SvelteElement} node @@ -39,11 +44,14 @@ export function SvelteElement(node, context) { const optimiser = new PromiseOptimiser(); + /** @type {Statement[]} */ + let statements = []; + build_element_attributes(node, { ...context, state }, optimiser.transform); if (dev) { const location = /** @type {Location} */ (locator(node.start)); - context.state.template.push( + statements.push( b.stmt( b.call( '$.push_element', @@ -74,9 +82,21 @@ export function SvelteElement(node, context) { statement = create_child_block(b.block([optimiser.apply(), statement]), true); } - context.state.template.push(statement); + statements.push(statement); if (dev) { - context.state.template.push(b.stmt(b.call('$.pop_element'))); + statements.push(b.stmt(b.call('$.pop_element'))); } + + if (node.metadata.expression.is_async()) { + statements = [ + create_async_block( + b.block(statements), + node.metadata.expression.blockers(), + node.metadata.expression.has_await + ) + ]; + } + + context.state.template.push(...statements); } diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js index d20792c91f..fa7b0116d2 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -104,10 +104,27 @@ export class Renderer { * @param {(renderer: Renderer) => void} fn */ async(blockers, fn) { + let callback = fn; + + if (blockers.length > 0) { + const context = ssr_context; + + callback = (renderer) => { + return Promise.all(blockers).then(() => { + const previous_context = ssr_context; + + try { + set_ssr_context(context); + return fn(renderer); + } finally { + set_ssr_context(previous_context); + } + }); + }; + } + this.#out.push(BLOCK_OPEN); - this.child( - blockers.length > 0 ? (renderer) => Promise.all(blockers).then(() => fn(renderer)) : fn - ); + this.child(callback); this.#out.push(BLOCK_CLOSE); } @@ -122,13 +139,13 @@ export class Renderer { for (const fn of thunks.slice(1)) { promise = promise.then(() => { - const previous_ssr_context = ssr_context; + const previous_context = ssr_context; set_ssr_context(context); try { return fn(); } finally { - set_ssr_context(previous_ssr_context); + set_ssr_context(previous_context); } }); } From eccebf01b631805a39d29d44d9067c7349aeb0dc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 22:49:58 -0400 Subject: [PATCH 39/56] WIP --- .../3-transform/server/transform-server.js | 2 +- .../server/visitors/shared/component.js | 8 ++++-- .../server/visitors/shared/utils.js | 27 ++++++++++++++++++- .../svelte/src/internal/server/renderer.js | 2 ++ 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index b6c3f3dc7b..89dfe85f26 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -41,7 +41,7 @@ import { TitleElement } from './visitors/TitleElement.js'; import { UpdateExpression } from './visitors/UpdateExpression.js'; import { VariableDeclaration } from './visitors/VariableDeclaration.js'; import { SvelteBoundary } from './visitors/SvelteBoundary.js'; -import { call_component_renderer, create_async_block } from './visitors/shared/utils.js'; +import { call_component_renderer } from './visitors/shared/utils.js'; /** @type {Visitors} */ const global_visitors = { 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 e463ea785a..e0baee9250 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 @@ -323,8 +323,12 @@ export function build_inline_component(node, expression, context) { ); } - if (optimiser.expressions.length > 0) { - statement = create_async_block(b.block([optimiser.apply(), statement])); + if (optimiser.is_async()) { + statement = create_async_block( + b.block([optimiser.apply(), statement]), + optimiser.blockers(), + optimiser.has_await + ); } if (dynamic && custom_css_props.length === 0) { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js index 2fff258411..9d4da0b006 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js @@ -1,5 +1,5 @@ /** @import { Expression, Identifier, Node, Statement, BlockStatement, ArrayExpression } from 'estree' */ -/** @import { AST } from '#compiler' */ +/** @import { AST, Binding } from '#compiler' */ /** @import { ComponentContext, ServerTransformState } from '../../types.js' */ import { escape_html } from '../../../../../../escaping.js'; @@ -299,13 +299,26 @@ export class PromiseOptimiser { /** @type {Expression[]} */ expressions = []; + has_await = false; + + /** @type {Set} */ + #blockers = new Set(); + /** * * @param {Expression} expression * @param {ExpressionMetadata} metadata */ transform = (expression, metadata) => { + for (const binding of metadata.dependencies) { + if (binding.blocker) { + this.#blockers.add(binding.blocker); + } + } + if (metadata.has_await) { + this.has_await = true; + const length = this.expressions.push(expression); return b.id(`$$${length - 1}`); } @@ -314,6 +327,10 @@ export class PromiseOptimiser { }; apply() { + if (this.expressions.length === 0) { + return b.empty; + } + if (this.expressions.length === 1) { return b.const('$$0', this.expressions[0]); } @@ -331,4 +348,12 @@ export class PromiseOptimiser { b.await(b.call('Promise.all', promises)) ); } + + blockers() { + return b.array([...this.#blockers]); + } + + is_async() { + return this.expressions.length > 0 || this.#blockers.size > 0; + } } diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js index fa7b0116d2..3562b5a60d 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -148,6 +148,8 @@ export class Renderer { set_ssr_context(previous_context); } }); + + promises.push(promise); } return promises; From 8638d22ac7d72769c68744a6f9a814fb9a6846e4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 23:00:00 -0400 Subject: [PATCH 40/56] WIP --- .../client/visitors/shared/component.js | 25 +++++++++++-------- .../src/internal/client/dom/blocks/async.js | 2 +- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 6d5ae926bd..55cff6cc37 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -16,12 +16,12 @@ import { determine_slot } from '../../../../../utils/slot.js'; * @returns {Statement} */ export function build_component(node, component_name, context) { - /** - * @type {Expression} - */ + /** @type {Expression} */ const anchor = context.state.node; + /** @type {Array} */ const props_and_spreads = []; + /** @type {Array<() => void>} */ const delayed_props = []; @@ -128,13 +128,16 @@ export function build_component(node, component_name, context) { (events[attribute.name] ||= []).push(handler); } else if (attribute.type === 'SpreadAttribute') { - const expression = /** @type {Expression} */ (context.visit(attribute)); + const expression = memoizer.add( + /** @type {Expression} */ (context.visit(attribute)), + attribute.metadata.expression + ); if (attribute.metadata.expression.has_state || attribute.metadata.expression.has_await) { props_and_spreads.push( b.thunk( attribute.metadata.expression.has_await || attribute.metadata.expression.has_call - ? b.call('$.get', memoizer.add(expression, attribute.metadata.expression)) + ? b.call('$.get', expression) : expression ) ); @@ -147,10 +150,10 @@ export function build_component(node, component_name, context) { b.init( attribute.name, build_attribute_value(attribute.value, context, (value, metadata) => { + const memoized = memoizer.add(value, metadata); + // TODO put the derived in the local block - return metadata.has_call || metadata.has_await - ? b.call('$.get', memoizer.add(value, metadata)) - : value; + return metadata.has_call || metadata.has_await ? b.call('$.get', memoized) : memoized; }).value ) ); @@ -184,9 +187,9 @@ export function build_component(node, component_name, context) { ); }); - return should_wrap_in_derived - ? b.call('$.get', memoizer.add(value, metadata, true)) - : value; + const memoized = memoizer.add(value, metadata, should_wrap_in_derived); + + return should_wrap_in_derived ? b.call('$.get', memoized) : memoized; } ); diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index e8412e6cab..db4ebc5934 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -18,7 +18,7 @@ import { get_boundary } from './boundary.js'; * @param {Array<() => Promise>} expressions * @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn */ -export function async(node, blockers, expressions, fn) { +export function async(node, blockers, expressions = [], fn) { var boundary = get_boundary(); var batch = /** @type {Batch} */ (current_batch); var blocking = !boundary.is_pending(); From 75867c43a47982141c3a7e45c77aac6225ad9309 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 23:09:19 -0400 Subject: [PATCH 41/56] WIP --- .../3-transform/client/visitors/RenderTag.js | 4 +- .../3-transform/server/visitors/AwaitBlock.js | 8 +++- .../3-transform/server/visitors/RenderTag.js | 38 +++++++++++++------ .../server/visitors/SlotElement.js | 11 ++++-- 4 files changed, 42 insertions(+), 19 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js index d6834c5ab0..71e8b2ad77 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js @@ -23,10 +23,10 @@ export function RenderTag(node, context) { const arg = /** @type {Expression} */ (call.arguments[i]); const metadata = node.metadata.arguments[i]; - let expression = build_expression(context, arg, metadata); + let expression = memoizer.add(build_expression(context, arg, metadata), metadata); if (metadata.has_await || metadata.has_call) { - expression = b.call('$.get', memoizer.add(expression, metadata)); + expression = b.call('$.get', expression); } args.push(b.thunk(expression)); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js index 132a83ec60..b8d2e42144 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js @@ -25,8 +25,12 @@ export function AwaitBlock(node, context) { ) ); - if (node.metadata.expression.has_await) { - statement = create_async_block(b.block([statement])); + if (node.metadata.expression.is_async()) { + statement = create_async_block( + b.block([statement]), + node.metadata.expression.blockers(), + node.metadata.expression.has_await + ); } context.state.template.push(statement, block_close); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RenderTag.js index 4ec686bb89..6d7cef0d95 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RenderTag.js @@ -3,32 +3,48 @@ /** @import { ComponentContext } from '../types.js' */ import { unwrap_optional } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; -import { empty_comment } from './shared/utils.js'; +import { create_async_block, empty_comment, PromiseOptimiser } from './shared/utils.js'; /** * @param {AST.RenderTag} node * @param {ComponentContext} context */ export function RenderTag(node, context) { + const optimiser = new PromiseOptimiser(); + const callee = unwrap_optional(node.expression).callee; const raw_args = unwrap_optional(node.expression).arguments; - const snippet_function = /** @type {Expression} */ (context.visit(callee)); + const snippet_function = optimiser.transform( + /** @type {Expression} */ (context.visit(callee)), + node.metadata.expression + ); - const snippet_args = raw_args.map((arg) => { - return /** @type {Expression} */ (context.visit(arg)); + const snippet_args = raw_args.map((arg, i) => { + return optimiser.transform( + /** @type {Expression} */ (context.visit(arg)), + node.metadata.arguments[i] + ); }); - context.state.template.push( - b.stmt( - (node.expression.type === 'CallExpression' ? b.call : b.maybe_call)( - snippet_function, - b.id('$$renderer'), - ...snippet_args - ) + let statement = b.stmt( + (node.expression.type === 'CallExpression' ? b.call : b.maybe_call)( + snippet_function, + b.id('$$renderer'), + ...snippet_args ) ); + if (optimiser.is_async()) { + statement = create_async_block( + b.block([optimiser.apply(), statement]), + optimiser.blockers(), + optimiser.has_await + ); + } + + context.state.template.push(statement); + if (!context.state.skip_hydration_boundaries) { context.state.template.push(empty_comment); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js index 5841aa44d8..d0f8e25d02 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js @@ -65,10 +65,13 @@ export function SlotElement(node, context) { fallback ); - const statement = - optimiser.expressions.length > 0 - ? create_async_block(b.block([optimiser.apply(), b.stmt(slot)])) - : b.stmt(slot); + const statement = optimiser.is_async() + ? create_async_block( + b.block([optimiser.apply(), b.stmt(slot)]), + optimiser.blockers(), + optimiser.has_await + ) + : b.stmt(slot); context.state.template.push(block_open, statement, block_close); } From 7aae17be24f1b1bdbadbc5ad860fb7321eaf7afc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 23:10:50 -0400 Subject: [PATCH 42/56] deprecate --- packages/svelte/src/compiler/types/template.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index fa7484e523..5c7f8cdb13 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -48,6 +48,7 @@ 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; }; } From 96ad53925130051db2bf3fd3ce9cb1fab7679f9a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 23:12:16 -0400 Subject: [PATCH 43/56] update tests --- .../_expected/server/index.svelte.js | 2 +- .../async-each-hoisting/_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../samples/async-if-hoisting/_expected/server/index.svelte.js | 2 +- .../samples/async-in-derived/_expected/server/index.svelte.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) 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 c579fda929..99c788da5e 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 @@ -2,7 +2,7 @@ import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/server'; export default function Async_each_fallback_hoisting($$renderer) { - $$renderer.async(async ($$renderer) => { + $$renderer.async([], async ($$renderer) => { const each_array = $.ensure_array_like((await $.save(Promise.resolve([])))()); if (each_array.length !== 0) { 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 7f39e64d8e..ee53e3c10a 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 @@ -8,7 +8,7 @@ export default function Async_each_hoisting($$renderer) { $$renderer.push(``); - $$renderer.async(async ($$renderer) => { + $$renderer.async([], async ($$renderer) => { const each_array = $.ensure_array_like((await $.save(Promise.resolve([first, second, third])))()); for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$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 df4ad80899..1855af6500 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 @@ -2,7 +2,7 @@ import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/server'; export default function Async_if_alternate_hoisting($$renderer) { - $$renderer.async(async ($$renderer) => { + $$renderer.async([], async ($$renderer) => { if ((await $.save(Promise.resolve(false)))()) { $$renderer.push(''); $$renderer.push(async () => $.escape(await Promise.reject('no no no'))); 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 1d935f9be8..a74b59e895 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 @@ -2,7 +2,7 @@ import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/server'; export default function Async_if_hoisting($$renderer) { - $$renderer.async(async ($$renderer) => { + $$renderer.async([], async ($$renderer) => { if ((await $.save(Promise.resolve(true)))()) { $$renderer.push(''); $$renderer.push(async () => $.escape(await Promise.resolve('yes yes yes'))); 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 ac542bfa0f..e9048861aa 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,7 +18,7 @@ export default function Async_in_derived($$renderer, $$props) { } ]); - $$renderer.async(async ($$renderer) => { + $$renderer.async([], async ($$renderer) => { if (true) { $$renderer.push(''); From c8c030af220e6d4b288bed33fc4937b48ad16931 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 23:13:10 -0400 Subject: [PATCH 44/56] lint --- .../src/compiler/phases/3-transform/server/transform-server.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 89dfe85f26..ba7860171c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -407,7 +407,8 @@ export function server_module(analysis, options) { // to be present for `javascript_visitors_legacy` and so is included in module // transform state as well as component transform state legacy_reactive_statements: new Map(), - state_fields: new Map() + state_fields: new Map(), + is_instance: false }; const module = /** @type {ESTree.Program} */ ( From 61f6a15efe6b2c57e6b2de7a85fe949c4fd1981b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 23:14:31 -0400 Subject: [PATCH 45/56] lint --- packages/svelte/src/compiler/phases/scope.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 8f34176ec7..9d99ea9ee6 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -1,4 +1,4 @@ -/** @import { BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator, Super } from 'estree' */ +/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator, Super } from 'estree' */ /** @import { Context, Visitor } from 'zimmerframe' */ /** @import { AST, BindingKind, DeclarationKind } from '#compiler' */ import is_reference from 'is-reference'; From c3346535f598b25e4ae4723ce59af8f0d9d81e1f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 23:34:14 -0400 Subject: [PATCH 46/56] WIP --- .../src/compiler/phases/2-analyze/index.js | 33 ++++++++++++++----- .../3-transform/client/visitors/Program.js | 2 +- .../3-transform/server/visitors/Program.js | 2 +- .../svelte/src/compiler/phases/types.d.ts | 3 +- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index c0acd0546c..10753729b3 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -691,9 +691,14 @@ export function analyze_component(root, source, options) { if (instance.has_await) { /** * @param {ESTree.Node} node - * @param {Set} dependencies + * @param {Set} seen + * @param {Set} reads + * @param {Set} writes */ - const trace_dependencies = (node, dependencies) => { + const trace_references = (node, reads, writes, seen = new Set()) => { + if (seen.has(node)) return; + seen.add(node); + walk( node, { scope: instance.scope }, @@ -706,33 +711,45 @@ export function analyze_component(root, source, options) { context.next(); } }, + AssignmentExpression(node, context) { + // TODO mark writes + }, + CallExpression(node, context) { + // TODO deopt arguments, assume they are mutated + // TODO recurse into function definitions + }, Identifier(node, context) { const parent = /** @type {ESTree.Node} */ (context.path.at(-1)); if (is_reference(node, parent)) { const binding = context.state.scope.get(node.name); if (binding) { - dependencies.add(binding); + reads.add(binding); } - - // TODO recurse into function definitions } } } ); - - return dependencies; }; /** * @param {ESTree.Statement | ESTree.VariableDeclarator | ESTree.FunctionDeclaration | ESTree.ClassDeclaration} node */ const push = (node) => { + /** @type {Set} */ + const reads = new Set(); + + /** @type {Set} */ + const writes = new Set(); + + trace_references(node, reads, writes); + /** @type {AwaitedStatement} */ const statement = { node, has_await: has_await_expression(node), declarations: [], - dependencies: trace_dependencies(node, new Set()) + reads, + writes }; analysis.awaited_statements.set(node, statement); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js index 327e80f29a..bd07e4bd07 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -236,7 +236,7 @@ function transform_body(program, context) { // find the earliest point we can insert this derived let index = -1; - for (const binding of derived.dependencies) { + for (const binding of derived.reads) { index = Math.max( index, statements.findIndex((s) => s.declarations.includes(binding)) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js index 4d4d8deae9..5f3fa33394 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js @@ -106,7 +106,7 @@ function transform_body(program, context) { // find the earliest point we can insert this derived let index = -1; - for (const binding of derived.dependencies) { + for (const binding of derived.reads) { index = Math.max( index, statements.findIndex((s) => s.declarations.includes(binding)) diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index 2a3617b2f1..c36b90a723 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -46,7 +46,8 @@ export interface AwaitedStatement { node: Statement | VariableDeclarator | ClassDeclaration | FunctionDeclaration; has_await: boolean; declarations: Binding[]; - dependencies: Set; + reads: Set; + writes: Set; } /** From f91ccc6b0ac98c50851f6343f31afa6dcac257ee Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 26 Oct 2025 08:07:47 -0400 Subject: [PATCH 47/56] WIP --- .../3-transform/client/visitors/Program.js | 17 +++++++++++++---- .../3-transform/server/visitors/Program.js | 17 +++++++++++++---- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js index bd07e4bd07..fdda4c9b2e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -252,6 +252,8 @@ function transform_body(program, context) { } } + var promises = b.id('$$promises'); // TODO if we use this technique for fragments, need to deconflict + if (statements.length > 0) { var declarations = statements.map((s) => s.declarations).flat(); @@ -322,14 +324,12 @@ function transform_body(program, context) { return b.thunk(b.block([/** @type {Statement} */ (context.visit(s.node))]), s.has_await); }); - var id = b.id('$$promises'); // TODO if we use this technique for fragments, need to deconflict - - out.push(b.var(id, b.call('$.run', b.array(thunks)))); + out.push(b.var(promises, b.call('$.run', b.array(thunks)))); for (let i = 0; i < statements.length; i += 1) { const s = statements[i]; - var blocker = b.member(id, b.literal(i), true); + var blocker = b.member(promises, b.literal(i), true); for (const binding of s.declarations) { binding.blocker = blocker; @@ -341,6 +341,15 @@ function transform_body(program, context) { // a synchronous `{obj.foo}` will fail } + for (const binding of context.state.scope.declarations.values()) { + // if the binding is updated (TODO or passed to a function, in which case it + // could be mutated), play it safe and block until the end. In future we + // could develop more sophisticated static analysis to optimise further + if (binding.updated) { + binding.blocker = b.member(promises, b.literal(statements.length - 1), true); + } + } + // console.log('statements', statements); // console.log('deriveds', deriveds); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js index 5f3fa33394..7bbe40db4f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js @@ -122,6 +122,8 @@ function transform_body(program, context) { } } + var promises = b.id('$$promises'); // TODO if we use this technique for fragments, need to deconflict + if (statements.length > 0) { var declarations = statements.map((s) => s.declarations).flat(); @@ -192,14 +194,12 @@ function transform_body(program, context) { return b.thunk(b.block([/** @type {Statement} */ (context.visit(s.node))]), s.has_await); }); - var id = b.id('$$promises'); // TODO if we use this technique for fragments, need to deconflict - - out.push(b.var(id, b.call('$$renderer.run', b.array(thunks)))); + out.push(b.var(promises, b.call('$$renderer.run', b.array(thunks)))); for (let i = 0; i < statements.length; i += 1) { const s = statements[i]; - var blocker = b.member(id, b.literal(i), true); + var blocker = b.member(promises, b.literal(i), true); for (const binding of s.declarations) { binding.blocker = blocker; @@ -211,6 +211,15 @@ function transform_body(program, context) { // a synchronous `{obj.foo}` will fail } + for (const binding of context.state.scope.declarations.values()) { + // if the binding is updated (TODO or passed to a function, in which case it + // could be mutated), play it safe and block until the end. In future we + // could develop more sophisticated static analysis to optimise further + if (binding.updated) { + binding.blocker = b.member(promises, b.literal(statements.length - 1), true); + } + } + // console.log('statements', statements); // console.log('deriveds', deriveds); From 33d37924cc6482fbeac0fbac714e6958ac12f5f3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 26 Oct 2025 10:15:34 -0400 Subject: [PATCH 48/56] fix --- .../3-transform/server/visitors/HtmlTag.js | 8 +++---- .../server/visitors/shared/utils.js | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js index 9a3d2830ac..a5e044d5d2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js @@ -2,6 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import * as b from '#compiler/builders'; +import { create_push } from './shared/utils.js'; /** * @param {AST.HtmlTag} node @@ -10,9 +11,6 @@ import * as b from '#compiler/builders'; export function HtmlTag(node, context) { const expression = /** @type {Expression} */ (context.visit(node.expression)); const call = b.call('$.html', expression); - context.state.template.push( - node.metadata.expression.has_await - ? b.stmt(b.call('$$renderer.push', b.thunk(call, true))) - : call - ); + + context.state.template.push(create_push(call, node.metadata.expression)); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js index 9d4da0b006..2d54ab0644 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js @@ -284,6 +284,27 @@ export function create_async_block(body, blockers = b.array([]), has_await = tru ); } +/** + * @param {Expression} expression + * @param {ExpressionMetadata} metadata + * @returns {Expression | Statement} + */ +export function create_push(expression, metadata) { + if (metadata.is_async()) { + let statement = b.stmt(b.call('$$renderer.push', b.thunk(expression, metadata.has_await))); + + const blockers = metadata.blockers(); + + if (blockers.elements.length > 0) { + statement = create_async_block(b.block([statement]), blockers, false); + } + + return statement; + } + + return expression; +} + /** * @param {BlockStatement | Expression} body * @param {Identifier | false} component_fn_id From 3fc998f1cd927aafed6ba931b4773f40a2c2bf18 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 26 Oct 2025 10:27:08 -0400 Subject: [PATCH 49/56] WIP --- .../3-transform/client/visitors/Program.js | 14 +++---- .../3-transform/server/visitors/HtmlTag.js | 2 +- .../3-transform/server/visitors/Program.js | 14 +++---- .../server/visitors/shared/utils.js | 24 ++++++----- .../svelte/src/internal/server/renderer.js | 7 ++-- .../_expected/server/index.svelte.js | 30 ++++++++------ .../_expected/server/index.svelte.js | 20 ++++++---- .../_expected/server/index.svelte.js | 22 +++++----- .../_expected/server/index.svelte.js | 22 +++++----- .../_expected/server/index.svelte.js | 40 ++++++++++--------- 10 files changed, 111 insertions(+), 84 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js index fdda4c9b2e..b5d94fb2ad 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -339,14 +339,14 @@ function transform_body(program, context) { // TODO we likely need to account for updates that happen after the declaration, // e.g. `let obj = $state()` followed by a later `obj = {...}`, otherwise // a synchronous `{obj.foo}` will fail - } - for (const binding of context.state.scope.declarations.values()) { - // if the binding is updated (TODO or passed to a function, in which case it - // could be mutated), play it safe and block until the end. In future we - // could develop more sophisticated static analysis to optimise further - if (binding.updated) { - binding.blocker = b.member(promises, b.literal(statements.length - 1), true); + for (const binding of context.state.scope.declarations.values()) { + // if the binding is updated (TODO or passed to a function, in which case it + // could be mutated), play it safe and block until the end. In future we + // could develop more sophisticated static analysis to optimise further + if (binding.updated) { + binding.blocker = b.member(promises, b.literal(statements.length - 1), true); + } } } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js index a5e044d5d2..08fd2133ee 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js @@ -12,5 +12,5 @@ export function HtmlTag(node, context) { const expression = /** @type {Expression} */ (context.visit(node.expression)); const call = b.call('$.html', expression); - context.state.template.push(create_push(call, node.metadata.expression)); + context.state.template.push(create_push(call, node.metadata.expression, true)); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js index 7bbe40db4f..3712783502 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js @@ -209,14 +209,14 @@ function transform_body(program, context) { // TODO we likely need to account for updates that happen after the declaration, // e.g. `let obj = $state()` followed by a later `obj = {...}`, otherwise // a synchronous `{obj.foo}` will fail - } - for (const binding of context.state.scope.declarations.values()) { - // if the binding is updated (TODO or passed to a function, in which case it - // could be mutated), play it safe and block until the end. In future we - // could develop more sophisticated static analysis to optimise further - if (binding.updated) { - binding.blocker = b.member(promises, b.literal(statements.length - 1), true); + for (const binding of context.state.scope.declarations.values()) { + // if the binding is updated (TODO or passed to a function, in which case it + // could be mutated), play it safe and block until the end. In future we + // could develop more sophisticated static analysis to optimise further + if (binding.updated) { + binding.blocker = b.member(promises, b.literal(statements.length - 1), true); + } } } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js index 2d54ab0644..8ba2dd1b07 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js @@ -77,12 +77,11 @@ export function process_children(nodes, { visit, state }) { } for (const node of nodes) { - if (node.type === 'ExpressionTag' && node.metadata.expression.has_await) { + if (node.type === 'ExpressionTag' && node.metadata.expression.is_async()) { flush(); - const visited = /** @type {Expression} */ (visit(node.expression)); - state.template.push( - b.stmt(b.call('$$renderer.push', b.thunk(b.call('$.escape', visited), true))) - ); + + const expression = /** @type {Expression} */ (visit(node.expression)); + state.template.push(create_push(b.call('$.escape', expression), node.metadata.expression)); } else if (node.type === 'Text' || node.type === 'Comment' || node.type === 'ExpressionTag') { sequence.push(node); } else { @@ -277,26 +276,33 @@ export function create_child_block(body, async) { * @param {BlockStatement | Expression} body * @param {ArrayExpression} blockers * @param {boolean} has_await + * @param {boolean} markers */ -export function create_async_block(body, blockers = b.array([]), has_await = true) { +export function create_async_block(body, blockers = b.array([]), has_await = true, markers = true) { return b.stmt( - b.call('$$renderer.async', blockers, b.arrow([b.id('$$renderer')], body, has_await)) + b.call( + '$$renderer.async', + blockers, + b.arrow([b.id('$$renderer')], body, has_await), + markers && b.true + ) ); } /** * @param {Expression} expression * @param {ExpressionMetadata} metadata + * @param {boolean} markers * @returns {Expression | Statement} */ -export function create_push(expression, metadata) { +export function create_push(expression, metadata, markers = false) { if (metadata.is_async()) { let statement = b.stmt(b.call('$$renderer.push', b.thunk(expression, metadata.has_await))); const blockers = metadata.blockers(); if (blockers.elements.length > 0) { - statement = create_async_block(b.block([statement]), blockers, false); + statement = create_async_block(b.block([statement]), blockers, false, markers); } return statement; diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js index 3562b5a60d..d60fe81d03 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -102,8 +102,9 @@ export class Renderer { /** * @param {Array>} blockers * @param {(renderer: Renderer) => void} fn + * @param {boolean} markers */ - async(blockers, fn) { + async(blockers, fn, markers) { let callback = fn; if (blockers.length > 0) { @@ -123,9 +124,9 @@ export class Renderer { }; } - this.#out.push(BLOCK_OPEN); + if (markers) this.#out.push(BLOCK_OPEN); this.child(callback); - this.#out.push(BLOCK_CLOSE); + if (markers) this.#out.push(BLOCK_CLOSE); } /** 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 99c788da5e..448267de8c 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 @@ -2,24 +2,28 @@ import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/server'; export default function Async_each_fallback_hoisting($$renderer) { - $$renderer.async([], async ($$renderer) => { - const each_array = $.ensure_array_like((await $.save(Promise.resolve([])))()); + $$renderer.async( + [], + async ($$renderer) => { + const each_array = $.ensure_array_like((await $.save(Promise.resolve([])))()); - if (each_array.length !== 0) { - $$renderer.push(''); + if (each_array.length !== 0) { + $$renderer.push(''); - for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) { - let item = each_array[$$index]; + for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) { + let item = each_array[$$index]; + $$renderer.push(``); + $$renderer.push(async () => $.escape(await Promise.reject('This should never be reached'))); + } + } else { + $$renderer.push(''); $$renderer.push(``); - $$renderer.push(async () => $.escape(await Promise.reject('This should never be reached'))); + $$renderer.push(async () => $.escape(await Promise.resolve(4))); } - } else { - $$renderer.push(''); - $$renderer.push(``); - $$renderer.push(async () => $.escape(await Promise.resolve(4))); - } - }); + }, + true + ); $$renderer.push(``); } \ No newline at end of file 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 ee53e3c10a..6d214df2d4 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 @@ -8,16 +8,20 @@ export default function Async_each_hoisting($$renderer) { $$renderer.push(``); - $$renderer.async([], async ($$renderer) => { - const each_array = $.ensure_array_like((await $.save(Promise.resolve([first, second, third])))()); + $$renderer.async( + [], + async ($$renderer) => { + 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]; + for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) { + let item = each_array[$$index]; - $$renderer.push(``); - $$renderer.push(async () => $.escape(await item)); - } - }); + $$renderer.push(``); + $$renderer.push(async () => $.escape(await item)); + } + }, + true + ); $$renderer.push(``); } \ No newline at end of file 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 1855af6500..3e0b50f3f2 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 @@ -2,15 +2,19 @@ import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/server'; export default function Async_if_alternate_hoisting($$renderer) { - $$renderer.async([], async ($$renderer) => { - if ((await $.save(Promise.resolve(false)))()) { - $$renderer.push(''); - $$renderer.push(async () => $.escape(await Promise.reject('no no no'))); - } else { - $$renderer.push(''); - $$renderer.push(async () => $.escape(await Promise.resolve('yes yes yes'))); - } - }); + $$renderer.async( + [], + async ($$renderer) => { + if ((await $.save(Promise.resolve(false)))()) { + $$renderer.push(''); + $$renderer.push(async () => $.escape(await Promise.reject('no no no'))); + } else { + $$renderer.push(''); + $$renderer.push(async () => $.escape(await Promise.resolve('yes yes yes'))); + } + }, + true + ); $$renderer.push(``); } \ No newline at end of file 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 a74b59e895..fadaec745f 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 @@ -2,15 +2,19 @@ import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/server'; export default function Async_if_hoisting($$renderer) { - $$renderer.async([], async ($$renderer) => { - if ((await $.save(Promise.resolve(true)))()) { - $$renderer.push(''); - $$renderer.push(async () => $.escape(await Promise.resolve('yes yes yes'))); - } else { - $$renderer.push(''); - $$renderer.push(async () => $.escape(await Promise.reject('no no no'))); - } - }); + $$renderer.async( + [], + async ($$renderer) => { + if ((await $.save(Promise.resolve(true)))()) { + $$renderer.push(''); + $$renderer.push(async () => $.escape(await Promise.resolve('yes yes yes'))); + } else { + $$renderer.push(''); + $$renderer.push(async () => $.escape(await Promise.reject('no no no'))); + } + }, + true + ); $$renderer.push(``); } \ No newline at end of file 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 e9048861aa..49b8bf9cda 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,28 @@ export default function Async_in_derived($$renderer, $$props) { } ]); - $$renderer.async([], 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(''); - } - }); + $$renderer.async( + [], + 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(''); + } + }, + true + ); $$renderer.push(``); }); From df31c4032423c2772a8ef6022dd3aeccc174b6cf Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 26 Oct 2025 10:29:05 -0400 Subject: [PATCH 50/56] unused --- .../phases/3-transform/server/visitors/shared/component.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 e0baee9250..dbd28d4adc 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 @@ -5,8 +5,7 @@ import { empty_comment, build_attribute_value, create_async_block, - PromiseOptimiser, - build_template + PromiseOptimiser } from './utils.js'; import * as b from '#compiler/builders'; import { is_element_node } from '../../../../nodes.js'; From 2e571e3dbf6b8e61cb37c6552cd1507348f8b041 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 26 Oct 2025 11:40:53 -0400 Subject: [PATCH 51/56] deopt to ensure state is ready --- .../src/compiler/phases/2-analyze/index.js | 82 ++++++++++++++++++- .../3-transform/client/visitors/Program.js | 18 +--- .../3-transform/server/visitors/Program.js | 18 +--- packages/svelte/src/compiler/phases/scope.js | 26 ++++-- .../samples/async-derived-module/_config.js | 6 +- .../samples/async-derived/_config.js | 6 +- 6 files changed, 110 insertions(+), 46 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 10753729b3..73bcdfc61d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -6,7 +6,12 @@ import { walk } from 'zimmerframe'; import { parse } from '../1-parse/acorn.js'; import * as e from '../../errors.js'; import * as w from '../../warnings.js'; -import { extract_identifiers, has_await_expression } from '../../utils/ast.js'; +import { + extract_identifiers, + has_await_expression, + object, + unwrap_pattern +} from '../../utils/ast.js'; import * as b from '#compiler/builders'; import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js'; import check_graph_for_cycles from './utils/check_graph_for_cycles.js'; @@ -689,6 +694,37 @@ export function analyze_component(root, source, options) { } if (instance.has_await) { + /** + * @param {ESTree.Expression} expression + * @param {Scope} scope + * @param {Set} touched + * @param {Set} seen + */ + const touch = (expression, scope, touched, seen = new Set()) => { + if (seen.has(expression)) return; + seen.add(expression); + + walk( + expression, + { scope }, + { + Identifier(node, context) { + const parent = /** @type {ESTree.Node} */ (context.path.at(-1)); + if (is_reference(node, parent)) { + const binding = context.state.scope.get(node.name); + if (binding) { + touched.add(binding); + + for (const assignment of binding.assignments) { + touch(assignment.value, assignment.scope, touched, seen); + } + } + } + } + } + ); + }; + /** * @param {ESTree.Node} node * @param {Set} seen @@ -699,6 +735,22 @@ export function analyze_component(root, source, options) { if (seen.has(node)) return; seen.add(node); + /** + * @param {ESTree.Pattern} node + * @param {Scope} scope + */ + function update(node, scope) { + for (const pattern of unwrap_pattern(node)) { + const node = object(pattern); + if (!node) return; + + const binding = scope.get(node.name); + if (!binding) return; + + writes.add(binding); + } + } + walk( node, { scope: instance.scope }, @@ -712,12 +764,34 @@ export function analyze_component(root, source, options) { } }, AssignmentExpression(node, context) { - // TODO mark writes + update(node.left, context.state.scope); + }, + UpdateExpression(node, context) { + update( + /** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node.argument), + context.state.scope + ); }, CallExpression(node, context) { - // TODO deopt arguments, assume they are mutated - // TODO recurse into function definitions + // for now, assume everything touched by the callee ends up mutating the object + // TODO optimise this better + + // special case — no need to peek inside effects + const rune = get_rune(node, context.state.scope); + if (rune === '$effect') return; + + /** @type {Set} */ + const touched = new Set(); + touch(node, context.state.scope, touched); + + for (const b of touched) { + writes.add(b); + } }, + // don't look inside functions until they are called + ArrowFunctionExpression(_, context) {}, + FunctionDeclaration(_, context) {}, + FunctionExpression(_, context) {}, Identifier(node, context) { const parent = /** @type {ESTree.Node} */ (context.path.at(-1)); if (is_reference(node, parent)) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js index b5d94fb2ad..603c7bd1a8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -334,24 +334,14 @@ function transform_body(program, context) { for (const binding of s.declarations) { binding.blocker = blocker; } - } - - // TODO we likely need to account for updates that happen after the declaration, - // e.g. `let obj = $state()` followed by a later `obj = {...}`, otherwise - // a synchronous `{obj.foo}` will fail - for (const binding of context.state.scope.declarations.values()) { - // if the binding is updated (TODO or passed to a function, in which case it - // could be mutated), play it safe and block until the end. In future we - // could develop more sophisticated static analysis to optimise further - if (binding.updated) { - binding.blocker = b.member(promises, b.literal(statements.length - 1), true); + for (const binding of s.writes) { + // if a statement writes to a binding, any reads of that + // binding must wait for the statement + binding.blocker = blocker; } } } - // console.log('statements', statements); - // console.log('deriveds', deriveds); - return out; } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js index 3712783502..a442939287 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js @@ -204,24 +204,14 @@ function transform_body(program, context) { for (const binding of s.declarations) { binding.blocker = blocker; } - } - - // TODO we likely need to account for updates that happen after the declaration, - // e.g. `let obj = $state()` followed by a later `obj = {...}`, otherwise - // a synchronous `{obj.foo}` will fail - for (const binding of context.state.scope.declarations.values()) { - // if the binding is updated (TODO or passed to a function, in which case it - // could be mutated), play it safe and block until the end. In future we - // could develop more sophisticated static analysis to optimise further - if (binding.updated) { - binding.blocker = b.member(promises, b.literal(statements.length - 1), true); + for (const binding of s.writes) { + // if a statement writes to a binding, any reads of that + // binding must wait for the statement + binding.blocker = blocker; } } } - // console.log('statements', statements); - // console.log('deriveds', deriveds); - return out; } diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 9d99ea9ee6..c39803318d 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -108,6 +108,9 @@ export class Binding { /** @type {Array<{ node: Identifier; path: AST.SvelteNode[] }>} */ references = []; + /** @type {Array<{ value: Expression; scope: Scope }>} */ + assignments = []; + /** * For `legacy_reactive`: its reactive dependencies * @type {Binding[]} @@ -152,6 +155,10 @@ export class Binding { this.initial = initial; this.kind = kind; this.declaration_kind = declaration_kind; + + if (initial) { + this.assignments.push({ value: /** @type {Expression} */ (initial), scope }); + } } get updated() { @@ -868,7 +875,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { /** @type {[Scope, { node: Identifier; path: AST.SvelteNode[] }][]} */ const references = []; - /** @type {[Scope, Pattern | MemberExpression][]} */ + /** @type {[Scope, Pattern | MemberExpression, Expression][]} */ const updates = []; /** @@ -1056,12 +1063,13 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { // updates AssignmentExpression(node, { state, next }) { - updates.push([state.scope, node.left]); + updates.push([state.scope, node.left, node.right]); next(); }, UpdateExpression(node, { state, next }) { - updates.push([state.scope, /** @type {Identifier | MemberExpression} */ (node.argument)]); + const expression = /** @type {Identifier | MemberExpression} */ (node.argument); + updates.push([state.scope, expression, expression]); next(); }, @@ -1282,10 +1290,11 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { }, BindDirective(node, context) { - updates.push([ - context.state.scope, - /** @type {Identifier | MemberExpression} */ (node.expression) - ]); + if (node.expression.type !== 'SequenceExpression') { + const expression = /** @type {Identifier | MemberExpression} */ (node.expression); + updates.push([context.state.scope, expression, expression]); + } + context.next(); }, @@ -1320,7 +1329,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { scope.reference(node, path); } - for (const [scope, node] of updates) { + for (const [scope, node, value] of updates) { for (const expression of unwrap_pattern(node)) { const left = object(expression); const binding = left && scope.get(left.name); @@ -1328,6 +1337,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { if (binding !== null && left !== binding.node) { if (left === expression) { binding.reassigned = true; + binding.assignments.push({ value, scope }); } else { binding.mutated = true; } diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js index 5ab7a9aa8c..7ab79eb825 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js @@ -67,15 +67,15 @@ export default test({ assert.deepEqual(logs, [ 'outside boundary 1', - 'template 42 1', '$effect.pre 42 1', '$effect 42 1', - 'template 84 2', + 'template 42 1', '$effect.pre 84 2', + 'template 84 2', 'outside boundary 2', '$effect 84 2', - 'template 86 2', '$effect.pre 86 2', + 'template 86 2', '$effect 86 2' ]); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js index a439b082e9..c866dce406 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js @@ -33,15 +33,15 @@ export default test({ assert.deepEqual(logs, [ 'outside boundary 1', - 'template 1a 1', '$effect.pre 1a 1', '$effect 1a 1', - 'template 2a 2', + 'template 1a 1', '$effect.pre 2a 2', + 'template 2a 2', 'outside boundary 2', '$effect 2a 2', - 'template 2b 2', '$effect.pre 2b 2', + 'template 2b 2', '$effect 2b 2' ]); } From 30b04a1e8c741534740dfdf0be7df83990caf420 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 26 Oct 2025 12:01:37 -0400 Subject: [PATCH 52/56] fix --- .../svelte/src/internal/client/reactivity/async.js | 12 ++++++++---- .../samples/async-derived-module/_config.js | 2 +- .../runtime-runes/samples/async-derived/_config.js | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index cd88f6e47e..75a71c31d9 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -345,10 +345,14 @@ export function run(thunks) { promises.push(promise); } - promise.then(() => { - boundary.update_pending_count(-1); - batch.decrement(blocking); - }); + promise + // wait one more tick, so that template effects are + // guaranteed to run before `$effect(...)` + .then(() => Promise.resolve()) + .then(() => { + boundary.update_pending_count(-1); + batch.decrement(blocking); + }); return promises; } diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js index 7ab79eb825..318f88bcc9 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js @@ -68,8 +68,8 @@ export default test({ assert.deepEqual(logs, [ 'outside boundary 1', '$effect.pre 42 1', - '$effect 42 1', 'template 42 1', + '$effect 42 1', '$effect.pre 84 2', 'template 84 2', 'outside boundary 2', diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js index c866dce406..7239643464 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js @@ -34,8 +34,8 @@ export default test({ assert.deepEqual(logs, [ 'outside boundary 1', '$effect.pre 1a 1', - '$effect 1a 1', 'template 1a 1', + '$effect 1a 1', '$effect.pre 2a 2', 'template 2a 2', 'outside boundary 2', From a77fab9d176b27cfb4752a42a4fe3027d9f393ce Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 26 Oct 2025 12:17:25 -0400 Subject: [PATCH 53/56] DRY --- .../3-transform/client/visitors/Program.js | 209 +---------------- .../3-transform/server/visitors/Program.js | 212 +----------------- .../3-transform/shared/transform-async.js | 200 +++++++++++++++++ 3 files changed, 221 insertions(+), 400 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js index 603c7bd1a8..1bb9c0c911 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -1,11 +1,10 @@ -/** @import { BlockStatement, ClassDeclaration, ClassExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, Program, Statement, VariableDeclaration, VariableDeclarator } from 'estree' */ +/** @import { Expression, ImportDeclaration, MemberExpression, Node, Program } from 'estree' */ /** @import { ComponentContext } from '../types' */ -/** @import { AwaitedStatement } from '../../../types' */ import { build_getter, is_prop_source } from '../utils.js'; import * as b from '#compiler/builders'; import { add_state_transformers } from './shared/declarations.js'; -import { get_rune } from '../../../scope.js'; import { runes } from '../../../../state.js'; +import { transform_body } from '../../shared/transform-async.js'; /** * @param {Program} node @@ -143,205 +142,15 @@ export function Program(node, context) { if (context.state.is_instance && runes) { return { ...node, - body: transform_body(node, context) + body: transform_body( + node, + context.state.analysis.awaited_statements, + b.id('$.run'), + (node) => /** @type {Node} */ (context.visit(node)), + (statement) => context.state.hoisted.push(statement) + ) }; } context.next(); } - -// TODO find a way to DRY out this and the corresponding server visitor -/** - * @param {Program} program - * @param {ComponentContext} context - */ -function transform_body(program, context) { - /** @type {Statement[]} */ - const out = []; - - /** @type {AwaitedStatement[]} */ - const statements = []; - - /** @type {AwaitedStatement[]} */ - const deriveds = []; - - const { awaited_statements } = context.state.analysis; - - let awaited = false; - - /** - * @param {Statement | VariableDeclarator | ClassDeclaration | FunctionDeclaration} node - */ - const push = (node) => { - const statement = awaited_statements.get(node); - - awaited ||= !!statement?.has_await; - - if (!awaited || !statement || node.type === 'FunctionDeclaration') { - if (node.type === 'VariableDeclarator') { - out.push(/** @type {VariableDeclaration} */ (context.visit(b.var(node.id, node.init)))); - } else { - out.push(/** @type {Statement} */ (context.visit(node))); - } - - return; - } - - // TODO put deriveds into a separate array, and group them immediately - // after their latest dependency. for now, to avoid having to figure - // out the intricacies of dependency tracking, just let 'em waterfall - // if (node.type === 'VariableDeclarator') { - // const rune = get_rune(node.init, context.state.scope); - - // if (rune === '$derived' || rune === '$derived.by') { - // deriveds.push(statement); - // return; - // } - // } - - statements.push(statement); - }; - - for (let node of program.body) { - if (node.type === 'ImportDeclaration') { - // TODO we can get rid of the visitor - context.state.hoisted.push(node); - continue; - } - - if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportAllDeclaration') { - // this can't happen, but it's useful for TypeScript to understand that - continue; - } - - if (node.type === 'ExportNamedDeclaration') { - if (node.declaration) { - // TODO ditto — no visitor needed - node = node.declaration; - } else { - continue; - } - } - - if (node.type === 'VariableDeclaration') { - for (const declarator of node.declarations) { - push(declarator); - } - } else { - push(node); - } - } - - for (const derived of deriveds) { - // find the earliest point we can insert this derived - let index = -1; - - for (const binding of derived.reads) { - index = Math.max( - index, - statements.findIndex((s) => s.declarations.includes(binding)) - ); - } - - if (index === -1 && !derived.has_await) { - const node = /** @type {VariableDeclarator} */ (derived.node); - out.push(/** @type {VariableDeclaration} */ (context.visit(b.var(node.id, node.init)))); - } else { - // TODO combine deriveds with Promise.all where necessary - statements.splice(index + 1, 0, derived); - } - } - - var promises = b.id('$$promises'); // TODO if we use this technique for fragments, need to deconflict - - if (statements.length > 0) { - var declarations = statements.map((s) => s.declarations).flat(); - - if (declarations.length > 0) { - out.push( - b.declaration( - 'var', - declarations.map((d) => b.declarator(d.node)) - ) - ); - } - - const thunks = statements.map((s) => { - if (s.node.type === 'VariableDeclarator') { - const visited = /** @type {VariableDeclaration} */ ( - context.visit(b.var(s.node.id, s.node.init)) - ); - - if (visited.declarations.length === 1) { - return b.thunk( - b.assignment('=', s.node.id, visited.declarations[0].init ?? b.void0), - s.has_await - ); - } - - // if we have multiple declarations, it indicates destructuring - return b.thunk( - b.block([ - b.var(visited.declarations[0].id, visited.declarations[0].init), - ...visited.declarations - .slice(1) - .map((d) => b.stmt(b.assignment('=', d.id, d.init ?? b.void0))) - ]), - s.has_await - ); - } - - if (s.node.type === 'ClassDeclaration') { - return b.thunk( - b.assignment( - '=', - s.node.id, - /** @type {ClassExpression} */ ({ ...s.node, type: 'ClassExpression' }) - ), - s.has_await - ); - } - - if (s.node.type === 'FunctionDeclaration') { - return b.thunk( - b.assignment( - '=', - s.node.id, - /** @type {FunctionExpression} */ ({ ...s.node, type: 'FunctionExpression' }) - ), - s.has_await - ); - } - - if (s.node.type === 'ExpressionStatement') { - const expression = /** @type {Expression} */ (context.visit(s.node.expression)); - - return expression.type === 'AwaitExpression' - ? b.thunk(expression, true) - : b.thunk(b.unary('void', expression), s.has_await); - } - - return b.thunk(b.block([/** @type {Statement} */ (context.visit(s.node))]), s.has_await); - }); - - out.push(b.var(promises, b.call('$.run', b.array(thunks)))); - - for (let i = 0; i < statements.length; i += 1) { - const s = statements[i]; - - var blocker = b.member(promises, b.literal(i), true); - - for (const binding of s.declarations) { - binding.blocker = blocker; - } - - for (const binding of s.writes) { - // if a statement writes to a binding, any reads of that - // binding must wait for the statement - binding.blocker = blocker; - } - } - } - - return out; -} diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js index a442939287..55fcba8c5f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js @@ -1,8 +1,8 @@ -/** @import { BlockStatement, ClassDeclaration, ClassExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, Program, Statement, VariableDeclaration, VariableDeclarator } from 'estree' */ +/** @import { Node, Program } from 'estree' */ /** @import { Context, ComponentContext } from '../types' */ -/** @import { AwaitedStatement } from '../../../types' */ import * as b from '#compiler/builders'; import { runes } from '../../../../state.js'; +import { transform_body } from '../../shared/transform-async.js'; /** * @param {Program} node @@ -10,208 +10,20 @@ import { runes } from '../../../../state.js'; */ export function Program(node, context) { if (context.state.is_instance && runes) { + // @ts-ignore wtf + const c = /** @type {ComponentContext} */ (context); + return { ...node, - // @ts-ignore wtf - body: transform_body(node, /** @type {ComponentContext} */ (context)) + body: transform_body( + node, + c.state.analysis.awaited_statements, + b.id('$$renderer.run'), + (node) => /** @type {Node} */ (context.visit(node)), + (statement) => c.state.hoisted.push(statement) + ) }; } context.next(); } - -// TODO find a way to DRY out this and the corresponding server visitor -/** - * @param {Program} program - * @param {ComponentContext} context - */ -function transform_body(program, context) { - /** @type {Statement[]} */ - const out = []; - - /** @type {AwaitedStatement[]} */ - const statements = []; - - /** @type {AwaitedStatement[]} */ - const deriveds = []; - - const { awaited_statements } = context.state.analysis; - - let awaited = false; - - /** - * @param {Statement | VariableDeclarator | ClassDeclaration | FunctionDeclaration} node - */ - const push = (node) => { - const statement = awaited_statements.get(node); - - awaited ||= !!statement?.has_await; - - if (!awaited || !statement || node.type === 'FunctionDeclaration') { - if (node.type === 'VariableDeclarator') { - out.push(/** @type {VariableDeclaration} */ (context.visit(b.var(node.id, node.init)))); - } else { - out.push(/** @type {Statement} */ (context.visit(node))); - } - - return; - } - - // TODO put deriveds into a separate array, and group them immediately - // after their latest dependency. for now, to avoid having to figure - // out the intricacies of dependency tracking, just let 'em waterfall - // if (node.type === 'VariableDeclarator') { - // const rune = get_rune(node.init, context.state.scope); - - // if (rune === '$derived' || rune === '$derived.by') { - // deriveds.push(statement); - // return; - // } - // } - - statements.push(statement); - }; - - for (let node of program.body) { - if (node.type === 'ImportDeclaration') { - // TODO we can get rid of the visitor - context.state.hoisted.push(node); - continue; - } - - if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportAllDeclaration') { - // this can't happen, but it's useful for TypeScript to understand that - continue; - } - - if (node.type === 'ExportNamedDeclaration') { - if (node.declaration) { - // TODO ditto — no visitor needed - node = node.declaration; - } else { - continue; - } - } - - if (node.type === 'VariableDeclaration') { - for (const declarator of node.declarations) { - push(declarator); - } - } else { - push(node); - } - } - - for (const derived of deriveds) { - // find the earliest point we can insert this derived - let index = -1; - - for (const binding of derived.reads) { - index = Math.max( - index, - statements.findIndex((s) => s.declarations.includes(binding)) - ); - } - - if (index === -1 && !derived.has_await) { - const node = /** @type {VariableDeclarator} */ (derived.node); - out.push(/** @type {VariableDeclaration} */ (context.visit(b.var(node.id, node.init)))); - } else { - // TODO combine deriveds with Promise.all where necessary - statements.splice(index + 1, 0, derived); - } - } - - var promises = b.id('$$promises'); // TODO if we use this technique for fragments, need to deconflict - - if (statements.length > 0) { - var declarations = statements.map((s) => s.declarations).flat(); - - if (declarations.length > 0) { - out.push( - b.declaration( - 'var', - declarations.map((d) => b.declarator(d.node)) - ) - ); - } - - const thunks = statements.map((s) => { - if (s.node.type === 'VariableDeclarator') { - const visited = /** @type {VariableDeclaration} */ ( - context.visit(b.var(s.node.id, s.node.init)) - ); - - if (visited.declarations.length === 1) { - return b.thunk( - b.assignment('=', s.node.id, visited.declarations[0].init ?? b.void0), - s.has_await - ); - } - - // if we have multiple declarations, it indicates destructuring - return b.thunk( - b.block([ - b.var(visited.declarations[0].id, visited.declarations[0].init), - ...visited.declarations - .slice(1) - .map((d) => b.stmt(b.assignment('=', d.id, d.init ?? b.void0))) - ]), - s.has_await - ); - } - - if (s.node.type === 'ClassDeclaration') { - return b.thunk( - b.assignment( - '=', - s.node.id, - /** @type {ClassExpression} */ ({ ...s.node, type: 'ClassExpression' }) - ), - s.has_await - ); - } - - if (s.node.type === 'FunctionDeclaration') { - return b.thunk( - b.assignment( - '=', - s.node.id, - /** @type {FunctionExpression} */ ({ ...s.node, type: 'FunctionExpression' }) - ), - s.has_await - ); - } - - if (s.node.type === 'ExpressionStatement') { - const expression = /** @type {Expression} */ (context.visit(s.node.expression)); - - return expression.type === 'AwaitExpression' - ? b.thunk(expression, true) - : b.thunk(b.unary('void', expression), s.has_await); - } - - return b.thunk(b.block([/** @type {Statement} */ (context.visit(s.node))]), s.has_await); - }); - - out.push(b.var(promises, b.call('$$renderer.run', b.array(thunks)))); - - for (let i = 0; i < statements.length; i += 1) { - const s = statements[i]; - - var blocker = b.member(promises, b.literal(i), true); - - for (const binding of s.declarations) { - binding.blocker = blocker; - } - - for (const binding of s.writes) { - // if a statement writes to a binding, any reads of that - // binding must wait for the statement - binding.blocker = blocker; - } - } - } - - return out; -} diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js b/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js new file mode 100644 index 0000000000..b77ecacea7 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js @@ -0,0 +1,200 @@ +/** @import * as ESTree from 'estree' */ +/** @import { AwaitedStatement } from '../../types' */ +import * as b from '#compiler/builders'; + +// TODO find a way to DRY out this and the corresponding server visitor +/** + * @param {ESTree.Program} program + * @param {Map} awaited_statements + * @param {ESTree.Expression} runner + * @param {(node: ESTree.Node) => ESTree.Node} transform + * @param {(node: ESTree.Statement | ESTree.ModuleDeclaration) => void} hoist + */ +export function transform_body(program, awaited_statements, runner, transform, hoist) { + /** @type {ESTree.Statement[]} */ + const out = []; + + /** @type {AwaitedStatement[]} */ + const statements = []; + + /** @type {AwaitedStatement[]} */ + const deriveds = []; + + let awaited = false; + + /** + * @param {ESTree.Statement | ESTree.VariableDeclarator | ESTree.ClassDeclaration | ESTree.FunctionDeclaration} node + */ + const push = (node) => { + const statement = awaited_statements.get(node); + + awaited ||= !!statement?.has_await; + + if (!awaited || !statement || node.type === 'FunctionDeclaration') { + if (node.type === 'VariableDeclarator') { + out.push(/** @type {ESTree.VariableDeclaration} */ (transform(b.var(node.id, node.init)))); + } else { + out.push(/** @type {ESTree.Statement} */ (transform(node))); + } + + return; + } + + // TODO put deriveds into a separate array, and group them immediately + // after their latest dependency. for now, to avoid having to figure + // out the intricacies of dependency tracking, just let 'em waterfall + // if (node.type === 'VariableDeclarator') { + // const rune = get_rune(node.init, context.state.scope); + + // if (rune === '$derived' || rune === '$derived.by') { + // deriveds.push(statement); + // return; + // } + // } + + statements.push(statement); + }; + + for (let node of program.body) { + if (node.type === 'ImportDeclaration') { + // TODO we can get rid of the visitor + hoist(node); + continue; + } + + if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportAllDeclaration') { + // this can't happen, but it's useful for TypeScript to understand that + continue; + } + + if (node.type === 'ExportNamedDeclaration') { + if (node.declaration) { + // TODO ditto — no visitor needed + node = node.declaration; + } else { + continue; + } + } + + if (node.type === 'VariableDeclaration') { + for (const declarator of node.declarations) { + push(declarator); + } + } else { + push(node); + } + } + + for (const derived of deriveds) { + // find the earliest point we can insert this derived + let index = -1; + + for (const binding of derived.reads) { + index = Math.max( + index, + statements.findIndex((s) => s.declarations.includes(binding)) + ); + } + + if (index === -1 && !derived.has_await) { + const node = /** @type {ESTree.VariableDeclarator} */ (derived.node); + out.push(/** @type {ESTree.VariableDeclaration} */ (transform(b.var(node.id, node.init)))); + } else { + // TODO combine deriveds with Promise.all where necessary + statements.splice(index + 1, 0, derived); + } + } + + var promises = b.id('$$promises'); // TODO if we use this technique for fragments, need to deconflict + + if (statements.length > 0) { + var declarations = statements.map((s) => s.declarations).flat(); + + if (declarations.length > 0) { + out.push( + b.declaration( + 'var', + declarations.map((d) => b.declarator(d.node)) + ) + ); + } + + const thunks = statements.map((s) => { + if (s.node.type === 'VariableDeclarator') { + const visited = /** @type {ESTree.VariableDeclaration} */ ( + transform(b.var(s.node.id, s.node.init)) + ); + + if (visited.declarations.length === 1) { + return b.thunk( + b.assignment('=', s.node.id, visited.declarations[0].init ?? b.void0), + s.has_await + ); + } + + // if we have multiple declarations, it indicates destructuring + return b.thunk( + b.block([ + b.var(visited.declarations[0].id, visited.declarations[0].init), + ...visited.declarations + .slice(1) + .map((d) => b.stmt(b.assignment('=', d.id, d.init ?? b.void0))) + ]), + s.has_await + ); + } + + if (s.node.type === 'ClassDeclaration') { + return b.thunk( + b.assignment( + '=', + s.node.id, + /** @type {ESTree.ClassExpression} */ ({ ...s.node, type: 'ClassExpression' }) + ), + s.has_await + ); + } + + if (s.node.type === 'FunctionDeclaration') { + return b.thunk( + b.assignment( + '=', + s.node.id, + /** @type {ESTree.FunctionExpression} */ ({ ...s.node, type: 'FunctionExpression' }) + ), + s.has_await + ); + } + + if (s.node.type === 'ExpressionStatement') { + const expression = /** @type {ESTree.Expression} */ (transform(s.node.expression)); + + return expression.type === 'AwaitExpression' + ? b.thunk(expression, true) + : b.thunk(b.unary('void', expression), s.has_await); + } + + return b.thunk(b.block([/** @type {ESTree.Statement} */ (transform(s.node))]), s.has_await); + }); + + out.push(b.var(promises, b.call(runner, b.array(thunks)))); + + for (let i = 0; i < statements.length; i += 1) { + const s = statements[i]; + + var blocker = b.member(promises, b.literal(i), true); + + for (const binding of s.declarations) { + binding.blocker = blocker; + } + + for (const binding of s.writes) { + // if a statement writes to a binding, any reads of that + // binding must wait for the statement + binding.blocker = blocker; + } + } + } + + return out; +} From df666c3ce3e530888535722f0fde466c03910174 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 26 Oct 2025 12:24:40 -0400 Subject: [PATCH 54/56] reduce diff --- .../compiler/phases/3-transform/client/visitors/DebugTag.js | 2 +- .../phases/3-transform/client/visitors/RegularElement.js | 3 +-- .../phases/3-transform/client/visitors/RenderTag.js | 2 +- .../phases/3-transform/client/visitors/SlotElement.js | 2 +- .../phases/3-transform/client/visitors/shared/component.js | 2 +- .../phases/3-transform/client/visitors/shared/element.js | 2 +- .../phases/3-transform/client/visitors/shared/utils.js | 6 +++--- packages/svelte/src/internal/client/dom/blocks/async.js | 2 +- packages/svelte/src/internal/client/dom/blocks/html.js | 2 +- .../svelte/src/internal/client/dom/elements/attributes.js | 2 +- packages/svelte/src/internal/client/reactivity/effects.js | 5 ++--- .../_expected/client/index.svelte.js | 4 ++-- .../async-each-hoisting/_expected/client/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 4 ++-- .../async-if-hoisting/_expected/client/index.svelte.js | 4 ++-- .../await-block-scope/_expected/client/index.svelte.js | 2 +- .../bind-component-snippet/_expected/client/index.svelte.js | 2 +- .../_expected/client/main.svelte.js | 3 +-- .../each-string-template/_expected/client/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 2 +- .../skip-static-subtree/_expected/client/index.svelte.js | 2 +- .../text-nodes-deriveds/_expected/client/index.svelte.js | 2 +- 23 files changed, 29 insertions(+), 32 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/DebugTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/DebugTag.js index 0711897c7b..ef9a070859 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/DebugTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/DebugTag.js @@ -23,6 +23,6 @@ export function DebugTag(node, context) { const call = b.call('console.log', object); context.state.init.push( - b.stmt(b.call('$.template_effect', b.array([]), b.thunk(b.block([b.stmt(call), b.debugger])))) + b.stmt(b.call('$.template_effect', b.thunk(b.block([b.stmt(call), b.debugger])))) ); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 264b8c3e34..3998770a71 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -476,7 +476,6 @@ function setup_select_synchronization(value_binding, context) { b.stmt( b.call( '$.template_effect', - b.array([]), b.thunk( b.block([b.stmt(/** @type {Expression} */ (context.visit(bound))), b.stmt(invalidator)]) ) @@ -655,7 +654,7 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co // this is different from other updates — it doesn't get grouped, // because set_custom_element_data may not be idempotent - const update = has_state ? b.call('$.template_effect', b.array([]), b.thunk(call)) : call; + const update = has_state ? b.call('$.template_effect', b.thunk(call)) : call; context.state.init.push(b.stmt(update)); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js index 71e8b2ad77..86b414d9ab 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js @@ -73,7 +73,7 @@ export function RenderTag(node, context) { const async_values = memoizer.async_values(); const blockers = memoizer.blockers(); - if (async_values || blockers.elements.length > 0) { + if (async_values || blockers) { context.state.init.push( b.stmt( b.call( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js index 5c3e59ad6c..294e5fdfc2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js @@ -76,7 +76,7 @@ export function SlotElement(node, context) { const async_values = memoizer.async_values(); const blockers = memoizer.blockers(); - if (async_values || blockers.elements.length > 0) { + if (async_values || blockers) { context.state.init.push( b.stmt( b.call( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 55cff6cc37..9a87c4c0e8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -502,7 +502,7 @@ export function build_component(node, component_name, context) { const async_values = memoizer.async_values(); const blockers = memoizer.blockers(); - if (async_values || blockers.elements.length > 0) { + if (async_values || blockers) { return b.stmt( b.call( '$.async', diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index eb38f50530..436d262d3a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -86,10 +86,10 @@ export function build_attribute_effect( b.call( '$.attribute_effect', element_id, - memoizer.blockers(), b.arrow(ids, b.object(values)), memoizer.sync_values(), memoizer.async_values(), + memoizer.blockers(), element.metadata.scoped && context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash), diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 7e71244dd5..83bc5da3e3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -60,7 +60,7 @@ export class Memoizer { } blockers() { - return b.array([...this.#blockers]); + return this.#blockers.size > 0 ? b.array([...this.#blockers]) : undefined; } deriveds(runes = true) { @@ -191,7 +191,6 @@ export function build_render_statement(state) { return b.stmt( b.call( '$.template_effect', - memoizer.blockers(), b.arrow( ids, state.update.length === 1 && state.update[0].type === 'ExpressionStatement' @@ -199,7 +198,8 @@ export function build_render_statement(state) { : b.block(state.update) ), memoizer.sync_values(), - memoizer.async_values() + memoizer.async_values(), + memoizer.blockers() ) ); } diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index db4ebc5934..d1c970fff7 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -18,7 +18,7 @@ import { get_boundary } from './boundary.js'; * @param {Array<() => Promise>} expressions * @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn */ -export function async(node, blockers, expressions = [], fn) { +export function async(node, blockers = [], expressions = [], fn) { var boundary = get_boundary(); var batch = /** @type {Batch} */ (current_batch); var blocking = !boundary.is_pending(); diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js index 2dd1da149b..d7190abc66 100644 --- a/packages/svelte/src/internal/client/dom/blocks/html.js +++ b/packages/svelte/src/internal/client/dom/blocks/html.js @@ -46,7 +46,7 @@ export function html(node, get_value, svg = false, mathml = false, skip_warning var value = ''; - template_effect([], () => { + template_effect(() => { var effect = /** @type {Effect} */ (active_effect); if (value === (value = get_value() ?? '')) { diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index a5ad3a31a8..97bad28be3 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -490,10 +490,10 @@ function set_attributes( */ export function attribute_effect( element, - blockers, fn, sync = [], async = [], + blockers = [], css_hash, should_remove_defaults = false, skip_warning = false diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index e50d7aa689..4a9fce7286 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -362,13 +362,12 @@ export function render_effect(fn, flags = 0) { } /** - * @param {Array>} blockers * @param {(...expressions: any) => void | (() => void)} fn * @param {Array<() => any>} sync * @param {Array<() => Promise>} async - * @param {Promise} [blocker] + * @param {Array>} blockers */ -export function template_effect(blockers, fn, sync = [], async = [], blocker) { +export function template_effect(fn, sync = [], async = [], blockers = []) { flatten(blockers, sync, async, (values) => { create_effect(RENDER_EFFECT, () => fn(...values.map(get)), true); }); diff --git a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js index 69d240cd17..6f1c40988d 100644 --- a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js @@ -17,7 +17,7 @@ export default function Async_each_fallback_hoisting($$anchor) { var text = $.text(); - $.template_effect([], ($0) => $.set_text(text, $0), void 0, [() => Promise.reject('This should never be reached')]); + $.template_effect(($0) => $.set_text(text, $0), void 0, [() => Promise.reject('This should never be reached')]); $.append($$anchor, text); }, ($$anchor) => { @@ -25,7 +25,7 @@ export default function Async_each_fallback_hoisting($$anchor) { var text_1 = $.text(); - $.template_effect([], ($0) => $.set_text(text_1, $0), void 0, [() => Promise.resolve(4)]); + $.template_effect(($0) => $.set_text(text_1, $0), void 0, [() => Promise.resolve(4)]); $.append($$anchor, text_1); } ); diff --git a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js index 4805066dac..17a32e4cc7 100644 --- a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js @@ -15,7 +15,7 @@ export default function Async_each_hoisting($$anchor) { var text = $.text(); - $.template_effect([], ($0) => $.set_text(text, $0), void 0, [() => $.get(item)]); + $.template_effect(($0) => $.set_text(text, $0), void 0, [() => $.get(item)]); $.append($$anchor, text); }); }); diff --git a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js index 3657621791..d86001e273 100644 --- a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js @@ -10,14 +10,14 @@ export default function Async_if_alternate_hoisting($$anchor) { var consequent = ($$anchor) => { var text = $.text(); - $.template_effect([], ($0) => $.set_text(text, $0), void 0, [() => Promise.reject('no no no')]); + $.template_effect(($0) => $.set_text(text, $0), void 0, [() => Promise.reject('no no no')]); $.append($$anchor, text); }; var alternate = ($$anchor) => { var text_1 = $.text(); - $.template_effect([], ($0) => $.set_text(text_1, $0), void 0, [() => Promise.resolve('yes yes yes')]); + $.template_effect(($0) => $.set_text(text_1, $0), void 0, [() => Promise.resolve('yes yes yes')]); $.append($$anchor, text_1); }; diff --git a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js index ffeee0c470..5cdb6978d9 100644 --- a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js @@ -10,14 +10,14 @@ export default function Async_if_hoisting($$anchor) { var consequent = ($$anchor) => { var text = $.text(); - $.template_effect([], ($0) => $.set_text(text, $0), void 0, [() => Promise.resolve('yes yes yes')]); + $.template_effect(($0) => $.set_text(text, $0), void 0, [() => Promise.resolve('yes yes yes')]); $.append($$anchor, text); }; var alternate = ($$anchor) => { var text_1 = $.text(); - $.template_effect([], ($0) => $.set_text(text_1, $0), void 0, [() => Promise.reject('no no no')]); + $.template_effect(($0) => $.set_text(text_1, $0), void 0, [() => Promise.reject('no no no')]); $.append($$anchor, text_1); }; diff --git a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js index b756dbad94..a78d8911cd 100644 --- a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js @@ -26,7 +26,7 @@ export default function Await_block_scope($$anchor) { var text_1 = $.sibling(node); - $.template_effect([], () => { + $.template_effect(() => { $.set_text(text, `clicks: ${counter.count ?? ''}`); $.set_text(text_1, ` ${counter.count ?? ''}`); }); diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js index 8e05aa55b0..f3850d1471 100644 --- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js @@ -30,6 +30,6 @@ export default function Bind_component_snippet($$anchor) { var text_1 = $.sibling(node); - $.template_effect([], () => $.set_text(text_1, ` value: ${$.get(value) ?? ''}`)); + $.template_effect(() => $.set_text(text_1, ` value: ${$.get(value) ?? ''}`)); $.append($$anchor, fragment); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js index 1250633be2..6fb7cbf183 100644 --- a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js +++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js @@ -25,10 +25,9 @@ export default function Main($$anchor) { var svg_1 = $.sibling(div_1, 2); var custom_element_1 = $.sibling(svg_1, 2); - $.template_effect([], () => $.set_custom_element_data(custom_element_1, 'fooBar', y())); + $.template_effect(() => $.set_custom_element_data(custom_element_1, 'fooBar', y())); $.template_effect( - [], ($0, $1) => { $.set_attribute(div_1, 'foobar', $0); $.set_attribute(svg_1, 'viewBox', $1); diff --git a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js index 4ff7796e59..c0626bd416 100644 --- a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js @@ -11,7 +11,7 @@ export default function Each_string_template($$anchor) { var text = $.text(); - $.template_effect([], () => $.set_text(text, `${thing ?? ''}, `)); + $.template_effect(() => $.set_text(text, `${thing ?? ''}, `)); $.append($$anchor, text); }); diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js index 1556d59583..ff1ca35dac 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js @@ -20,7 +20,7 @@ export default function Function_prop_no_getter($$anchor) { var text = $.text(); - $.template_effect([], () => $.set_text(text, `clicks: ${$.get(count) ?? ''}`)); + $.template_effect(() => $.set_text(text, `clicks: ${$.get(count) ?? ''}`)); $.append($$anchor, text); }, diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js index f09a0af0e9..3cc5c882e1 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js @@ -26,7 +26,7 @@ export default function Nullish_coallescence_omittance($$anchor) { var h1_1 = $.sibling(button, 2); h1_1.textContent = 'Hello, world'; - $.template_effect([], () => $.set_text(text, `Count is ${$.get(count) ?? ''}`)); + $.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`)); $.append($$anchor, fragment); } diff --git a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js index 801edce055..78147659ff 100644 --- a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js @@ -44,6 +44,6 @@ export default function Skip_static_subtree($$anchor, $$props) { var img = $.sibling(select, 2); $.next(2); - $.template_effect([], () => $.set_text(text, $$props.title)); + $.template_effect(() => $.set_text(text, $$props.title)); $.append($$anchor, fragment); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js index 2e49b8af23..de05f6af20 100644 --- a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js @@ -19,6 +19,6 @@ export default function Text_nodes_deriveds($$anchor) { var text = $.child(p); $.reset(p); - $.template_effect([], ($0, $1) => $.set_text(text, `${$0 ?? ''}${$1 ?? ''}`), [text1, text2]); + $.template_effect(($0, $1) => $.set_text(text, `${$0 ?? ''}${$1 ?? ''}`), [text1, text2]); $.append($$anchor, p); } \ No newline at end of file From 7585da0686c0ad2218ecd76aead821fe426e64a9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 26 Oct 2025 12:27:11 -0400 Subject: [PATCH 55/56] reduce diff --- .../server/visitors/shared/utils.js | 5 +-- .../svelte/src/internal/server/renderer.js | 15 +++++-- .../_expected/server/index.svelte.js | 30 ++++++-------- .../_expected/server/index.svelte.js | 20 ++++------ .../_expected/server/index.svelte.js | 22 +++++----- .../_expected/server/index.svelte.js | 22 +++++----- .../_expected/server/index.svelte.js | 40 +++++++++---------- 7 files changed, 70 insertions(+), 84 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js index 8ba2dd1b07..b31ca94c6e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js @@ -281,10 +281,9 @@ export function create_child_block(body, async) { export function create_async_block(body, blockers = b.array([]), has_await = true, markers = true) { return b.stmt( b.call( - '$$renderer.async', + markers ? '$$renderer.async_block' : '$$renderer.async', blockers, - b.arrow([b.id('$$renderer')], body, has_await), - markers && b.true + b.arrow([b.id('$$renderer')], body, has_await) ) ); } diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js index d60fe81d03..5dc845e376 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -102,9 +102,18 @@ export class Renderer { /** * @param {Array>} blockers * @param {(renderer: Renderer) => void} fn - * @param {boolean} markers */ - async(blockers, fn, markers) { + async_block(blockers, fn) { + this.#out.push(BLOCK_OPEN); + this.async(blockers, fn); + this.#out.push(BLOCK_CLOSE); + } + + /** + * @param {Array>} blockers + * @param {(renderer: Renderer) => void} fn + */ + async(blockers, fn) { let callback = fn; if (blockers.length > 0) { @@ -124,9 +133,7 @@ export class Renderer { }; } - if (markers) this.#out.push(BLOCK_OPEN); this.child(callback); - if (markers) this.#out.push(BLOCK_CLOSE); } /** 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 448267de8c..7249fd6e4f 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 @@ -2,28 +2,24 @@ import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/server'; export default function Async_each_fallback_hoisting($$renderer) { - $$renderer.async( - [], - async ($$renderer) => { - const each_array = $.ensure_array_like((await $.save(Promise.resolve([])))()); + $$renderer.async_block([], async ($$renderer) => { + const each_array = $.ensure_array_like((await $.save(Promise.resolve([])))()); - if (each_array.length !== 0) { - $$renderer.push(''); + if (each_array.length !== 0) { + $$renderer.push(''); - for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) { - let item = each_array[$$index]; + for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) { + let item = each_array[$$index]; - $$renderer.push(``); - $$renderer.push(async () => $.escape(await Promise.reject('This should never be reached'))); - } - } else { - $$renderer.push(''); $$renderer.push(``); - $$renderer.push(async () => $.escape(await Promise.resolve(4))); + $$renderer.push(async () => $.escape(await Promise.reject('This should never be reached'))); } - }, - true - ); + } else { + $$renderer.push(''); + $$renderer.push(``); + $$renderer.push(async () => $.escape(await Promise.resolve(4))); + } + }); $$renderer.push(``); } \ No newline at end of file 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 6d214df2d4..10fa06e860 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 @@ -8,20 +8,16 @@ export default function Async_each_hoisting($$renderer) { $$renderer.push(``); - $$renderer.async( - [], - async ($$renderer) => { - const each_array = $.ensure_array_like((await $.save(Promise.resolve([first, second, third])))()); + $$renderer.async_block([], async ($$renderer) => { + 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]; + for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) { + let item = each_array[$$index]; - $$renderer.push(``); - $$renderer.push(async () => $.escape(await item)); - } - }, - true - ); + $$renderer.push(``); + $$renderer.push(async () => $.escape(await item)); + } + }); $$renderer.push(``); } \ No newline at end of file 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 3e0b50f3f2..1e7330429a 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 @@ -2,19 +2,15 @@ import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/server'; export default function Async_if_alternate_hoisting($$renderer) { - $$renderer.async( - [], - async ($$renderer) => { - if ((await $.save(Promise.resolve(false)))()) { - $$renderer.push(''); - $$renderer.push(async () => $.escape(await Promise.reject('no no no'))); - } else { - $$renderer.push(''); - $$renderer.push(async () => $.escape(await Promise.resolve('yes yes yes'))); - } - }, - true - ); + $$renderer.async_block([], async ($$renderer) => { + if ((await $.save(Promise.resolve(false)))()) { + $$renderer.push(''); + $$renderer.push(async () => $.escape(await Promise.reject('no no no'))); + } else { + $$renderer.push(''); + $$renderer.push(async () => $.escape(await Promise.resolve('yes yes yes'))); + } + }); $$renderer.push(``); } \ No newline at end of file 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 fadaec745f..1ca24cf81a 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 @@ -2,19 +2,15 @@ import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/server'; export default function Async_if_hoisting($$renderer) { - $$renderer.async( - [], - async ($$renderer) => { - if ((await $.save(Promise.resolve(true)))()) { - $$renderer.push(''); - $$renderer.push(async () => $.escape(await Promise.resolve('yes yes yes'))); - } else { - $$renderer.push(''); - $$renderer.push(async () => $.escape(await Promise.reject('no no no'))); - } - }, - true - ); + $$renderer.async_block([], async ($$renderer) => { + if ((await $.save(Promise.resolve(true)))()) { + $$renderer.push(''); + $$renderer.push(async () => $.escape(await Promise.resolve('yes yes yes'))); + } else { + $$renderer.push(''); + $$renderer.push(async () => $.escape(await Promise.reject('no no no'))); + } + }); $$renderer.push(``); } \ No newline at end of file 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 49b8bf9cda..bece6402c6 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,28 +18,24 @@ export default function Async_in_derived($$renderer, $$props) { } ]); - $$renderer.async( - [], - 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(''); - } - }, - true - ); + $$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(''); + } + }); $$renderer.push(``); }); From 5186155999acc7be75a8c3012dc6cea46676c5b5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 26 Oct 2025 12:30:16 -0400 Subject: [PATCH 56/56] reduce diff --- .../phases/3-transform/shared/transform-async.js | 11 +++++++++-- .../_expected/client/index.svelte.js | 6 +++--- .../_expected/server/index.svelte.js | 6 +++--- .../_expected/client/index.svelte.js | 4 ++-- .../_expected/server/index.svelte.js | 4 ++-- .../_expected/client/index.svelte.js | 4 ++-- .../_expected/server/index.svelte.js | 4 ++-- .../_expected/client/main.svelte.js | 6 +++--- .../_expected/server/main.svelte.js | 6 +++--- .../_expected/client/index.svelte.js | 4 ++-- .../_expected/server/index.svelte.js | 4 ++-- .../_expected/client/index.svelte.js | 4 ++-- .../_expected/server/index.svelte.js | 4 ++-- .../props-identifier/_expected/client/index.svelte.js | 2 +- .../props-identifier/_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 4 ++-- .../_expected/server/index.svelte.js | 4 ++-- .../svelte-element/_expected/client/index.svelte.js | 2 +- .../svelte-element/_expected/server/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 4 ++-- .../_expected/server/index.svelte.js | 4 ++-- 22 files changed, 50 insertions(+), 43 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js b/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js index b77ecacea7..88d8e8b982 100644 --- a/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js +++ b/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js @@ -77,8 +77,15 @@ export function transform_body(program, awaited_statements, runner, transform, h } if (node.type === 'VariableDeclaration') { - for (const declarator of node.declarations) { - push(declarator); + if ( + !awaited && + node.declarations.every((declarator) => !awaited_statements.get(declarator)?.has_await) + ) { + out.push(/** @type {ESTree.VariableDeclaration} */ (transform(node))); + } else { + for (const declarator of node.declarations) { + push(declarator); + } } } else { push(node); diff --git a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js index 17a32e4cc7..4045ad4bf4 100644 --- a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js @@ -3,9 +3,9 @@ import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/client'; export default function Async_each_hoisting($$anchor) { - var first = Promise.resolve(1); - var second = Promise.resolve(2); - var third = Promise.resolve(3); + const first = Promise.resolve(1); + const second = Promise.resolve(2); + const third = Promise.resolve(3); var fragment = $.comment(); var node = $.first_child(fragment); 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 10fa06e860..43fe9414eb 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 @@ -2,9 +2,9 @@ import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/server'; export default function Async_each_hoisting($$renderer) { - var first = Promise.resolve(1); - var second = Promise.resolve(2); - var third = Promise.resolve(3); + const first = Promise.resolve(1); + const second = Promise.resolve(2); + const third = Promise.resolve(3); $$renderer.push(``); diff --git a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js index a78d8911cd..52820c1652 100644 --- a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js @@ -4,8 +4,8 @@ import * as $ from 'svelte/internal/client'; var root = $.from_html(` `, 1); export default function Await_block_scope($$anchor) { - var counter = $.proxy({ count: 0 }); - var promise = $.derived(() => Promise.resolve(counter)); + let counter = $.proxy({ count: 0 }); + const promise = $.derived(() => Promise.resolve(counter)); function increment() { counter.count += 1; diff --git a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js index 98ca5214ec..e9bf215dcd 100644 --- a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js @@ -1,8 +1,8 @@ import * as $ from 'svelte/internal/server'; export default function Await_block_scope($$renderer) { - var counter = { count: 0 }; - var promise = Promise.resolve(counter); + let counter = { count: 0 }; + const promise = Promise.resolve(counter); function increment() { counter.count += 1; diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js index f3850d1471..a87a356d58 100644 --- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js @@ -13,8 +13,8 @@ const snippet = ($$anchor) => { var root = $.from_html(` `, 1); export default function Bind_component_snippet($$anchor) { - var value = $.state(''); - var _snippet = snippet; + let value = $.state(''); + const _snippet = snippet; var fragment = root(); var node = $.first_child(fragment); diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js index 8fb72c3da0..2ef3a429ba 100644 --- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js @@ -6,8 +6,8 @@ function snippet($$renderer) { } export default function Bind_component_snippet($$renderer) { - var value = ''; - var _snippet = snippet; + let value = ''; + const _snippet = snippet; let $$settled = true; let $$inner_renderer; diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js index 6fb7cbf183..d84b674f88 100644 --- a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js +++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js @@ -4,10 +4,10 @@ import * as $ from 'svelte/internal/client'; var root = $.from_html(`
`, 3); export default function Main($$anchor) { - var // needs to be a snapshot test because jsdom does auto-correct the attribute casing - x = 'test'; + // needs to be a snapshot test because jsdom does auto-correct the attribute casing + let x = 'test'; - var y = () => 'test'; + let y = () => 'test'; var fragment = root(); var div = $.first_child(fragment); diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js index b14eca288a..1ff8402974 100644 --- a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js +++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js @@ -1,10 +1,10 @@ import * as $ from 'svelte/internal/server'; export default function Main($$renderer) { - var // needs to be a snapshot test because jsdom does auto-correct the attribute casing - x = 'test'; + // needs to be a snapshot test because jsdom does auto-correct the attribute casing + let x = 'test'; - var y = () => 'test'; + let y = () => 'test'; $$renderer.push(` `); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js index ff1ca35dac..218951b836 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js @@ -2,13 +2,13 @@ import 'svelte/internal/disclose-version'; import * as $ from 'svelte/internal/client'; export default function Function_prop_no_getter($$anchor) { - var count = $.state(0); + let count = $.state(0); function onmouseup() { $.set(count, $.get(count) + 2); } - var plusOne = (num) => num + 1; + const plusOne = (num) => num + 1; Button($$anchor, { onmousedown: () => $.set(count, $.get(count) + 1), diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js index 9dc5acb576..855ae30d21 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js @@ -1,13 +1,13 @@ import * as $ from 'svelte/internal/server'; export default function Function_prop_no_getter($$renderer) { - var count = 0; + let count = 0; function onmouseup() { count += 2; } - var plusOne = (num) => num + 1; + const plusOne = (num) => num + 1; Button($$renderer, { onmousedown: () => count += 1, diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js index 3cc5c882e1..7025c788be 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js @@ -4,8 +4,8 @@ import * as $ from 'svelte/internal/client'; var root = $.from_html(`

`, 1); export default function Nullish_coallescence_omittance($$anchor) { - var name = 'world'; - var count = $.state(0); + let name = 'world'; + let count = $.state(0); var fragment = root(); var h1 = $.first_child(fragment); diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js index 5f979a8a69..a7e580acb8 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js @@ -1,8 +1,8 @@ import * as $ from 'svelte/internal/server'; export default function Nullish_coallescence_omittance($$renderer) { - var name = 'world'; - var count = 0; + let name = 'world'; + let count = 0; $$renderer.push(`

Hello, world!

123

Hello, world

`); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client/index.svelte.js index 2844643b45..5a46b9bbef 100644 --- a/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client/index.svelte.js @@ -4,7 +4,7 @@ import * as $ from 'svelte/internal/client'; export default function Props_identifier($$anchor, $$props) { $.push($$props, true); - var props = $.rest_props($$props, ['$$slots', '$$events', '$$legacy']); + let props = $.rest_props($$props, ['$$slots', '$$events', '$$legacy']); $$props.a; props[a]; diff --git a/packages/svelte/tests/snapshot/samples/props-identifier/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/server/index.svelte.js index e021288345..6db24ac621 100644 --- a/packages/svelte/tests/snapshot/samples/props-identifier/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/server/index.svelte.js @@ -2,7 +2,7 @@ import * as $ from 'svelte/internal/server'; export default function Props_identifier($$renderer, $$props) { $$renderer.component(($$renderer) => { - var { $$slots, $$events, ...props } = $$props; + let { $$slots, $$events, ...props } = $$props; props.a; props[a]; diff --git a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js index 56af168518..7a9f6193d7 100644 --- a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js @@ -1,7 +1,7 @@ import * as $ from 'svelte/internal/server'; export default function Skip_static_subtree($$renderer, $$props) { - var { title, content } = $$props; + let { title, content } = $$props; $$renderer.push(`

${$.escape(title)}

we don't need to traverse these nodes

or

these

ones

${$.html(content)}

these

trailing

nodes

can

be

completely

ignored

`, 1); export default function State_proxy_literal($$anchor) { - var str = $.state(''); - var tpl = $.state(``); + let str = $.state(''); + let tpl = $.state(``); function reset() { $.set(str, ''); diff --git a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js index 91efa4a946..4ab7f90c58 100644 --- a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js @@ -1,8 +1,8 @@ import * as $ from 'svelte/internal/server'; export default function State_proxy_literal($$renderer) { - var str = ''; - var tpl = ``; + let str = ''; + let tpl = ``; function reset() { str = ''; diff --git a/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js index 3b94b9533e..2270005ee0 100644 --- a/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js @@ -2,7 +2,7 @@ import 'svelte/internal/disclose-version'; import * as $ from 'svelte/internal/client'; export default function Svelte_element($$anchor, $$props) { - var tag = $.prop($$props, 'tag', 3, 'hr'); + let tag = $.prop($$props, 'tag', 3, 'hr'); var fragment = $.comment(); var node = $.first_child(fragment); diff --git a/packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js index c9e24b3190..fc97686bb1 100644 --- a/packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js @@ -1,7 +1,7 @@ import * as $ from 'svelte/internal/server'; export default function Svelte_element($$renderer, $$props) { - var { tag = 'hr' } = $$props; + let { tag = 'hr' } = $$props; $.element($$renderer, tag); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js index de05f6af20..464435cb0a 100644 --- a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js @@ -4,8 +4,8 @@ import * as $ from 'svelte/internal/client'; var root = $.from_html(`

`); export default function Text_nodes_deriveds($$anchor) { - var count1 = 0; - var count2 = 0; + let count1 = 0; + let count2 = 0; function text1() { return count1; diff --git a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js index 424fb44bf8..f886f9fbe3 100644 --- a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js @@ -1,8 +1,8 @@ import * as $ from 'svelte/internal/server'; export default function Text_nodes_deriveds($$renderer) { - var count1 = 0; - var count2 = 0; + let count1 = 0; + let count2 = 0; function text1() { return count1;