From 3e7589360a94eca01c547b7e9cb34006b53e7f88 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Oct 2025 13:17:52 -0400 Subject: [PATCH 01/85] 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 b4c704c34d81..5c653c7f2e05 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 4e287fd199c7..79fa4ab88e6a 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/85] 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 2d5b0c893184..d1aa27ea25ed 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/85] 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 d1aa27ea25ed..80cbb4e69048 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 1e4ac6960864..2fff25841189 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/85] 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 f1dc9fa1b426..1396aa8fada3 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/85] 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 2e61897924c1..145167a690f2 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 d20792c91f50..fa7b0116d20b 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/85] 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 b6c3f3dc7b5b..89dfe85f264a 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 e463ea785a63..e0baee92507b 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 2fff25841189..9d4da0b00684 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 fa7b0116d20b..3562b5a60d94 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/85] 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 6d5ae926bdbb..55cff6cc37d4 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 e8412e6cabbc..db4ebc5934ab 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/85] 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 d6834c5ab03f..71e8b2ad77d5 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 132a83ec6097..b8d2e421440a 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 4ec686bb89f6..6d7cef0d95a9 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 5841aa44d8c6..d0f8e25d021a 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/85] 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 fa7484e523ed..5c7f8cdb139e 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/85] 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 c579fda9290f..99c788da5e6a 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 7f39e64d8e15..ee53e3c10a65 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 df4ad808990e..1855af6500af 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 1d935f9be880..a74b59e895e9 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 ac542bfa0fbd..e9048861aaf9 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/85] 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 89dfe85f264a..ba7860171c2d 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/85] 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 8f34176ec760..9d99ea9ee6b3 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/85] 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 c0acd0546cc8..10753729b347 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 327e80f29a6c..bd07e4bd0784 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 4d4d8deae952..5f3fa33394f5 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 2a3617b2f1b3..c36b90a72379 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/85] 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 bd07e4bd0784..fdda4c9b2e08 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 5f3fa33394f5..7bbe40db4fcf 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/85] 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 9a3d2830ac36..a5e044d5d2e8 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 9d4da0b00684..2d54ab064484 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/85] 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 fdda4c9b2e08..b5d94fb2ad21 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 a5e044d5d2e8..08fd2133eef3 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 7bbe40db4fcf..37127835024e 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 2d54ab064484..8ba2dd1b0793 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 3562b5a60d94..d60fe81d0309 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 99c788da5e6a..448267de8c32 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 ee53e3c10a65..6d214df2d4d1 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 1855af6500af..3e0b50f3f2ec 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 a74b59e895e9..fadaec745f13 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 e9048861aaf9..49b8bf9cda4c 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/85] 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 e0baee92507b..dbd28d4adc76 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/85] 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 10753729b347..73bcdfc61d53 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 b5d94fb2ad21..603c7bd1a801 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 37127835024e..a442939287f0 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 9d99ea9ee6b3..c39803318d31 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 5ab7a9aa8c31..7ab79eb82584 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 a439b082e903..c866dce4066d 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/85] 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 cd88f6e47e84..75a71c31d97a 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 7ab79eb82584..318f88bcc9a6 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 c866dce4066d..72396434642e 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/85] 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 603c7bd1a801..1bb9c0c91189 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 a442939287f0..55fcba8c5fe1 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 000000000000..b77ecacea713 --- /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/85] 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 0711897c7b8c..ef9a070859ca 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 264b8c3e345a..3998770a717e 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 71e8b2ad77d5..86b414d9ab39 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 5c3e59ad6c3f..294e5fdfc234 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 55cff6cc37d4..9a87c4c0e862 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 eb38f50530ae..436d262d3a0b 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 7e71244dd52d..83bc5da3e349 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 db4ebc5934ab..d1c970fff7c8 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 2dd1da149b3c..d7190abc6668 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 a5ad3a31a854..97bad28be32f 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 e50d7aa689a2..4a9fce7286a5 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 69d240cd17f1..6f1c40988d90 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 4805066dac98..17a32e4cc720 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 365762179112..d86001e27315 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 ffeee0c470f4..5cdb6978d987 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 b756dbad94de..a78d8911cda3 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 8e05aa55b06e..f3850d14714b 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 1250633be20b..6fb7cbf18314 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 4ff7796e59b2..c0626bd416c9 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 1556d5958333..ff1ca35dacad 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 f09a0af0e97a..3cc5c882e11d 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 801edce0552d..78147659ff40 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 2e49b8af238c..de05f6af20e2 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/85] 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 8ba2dd1b0793..b31ca94c6ed5 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 d60fe81d0309..5dc845e37655 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 448267de8c32..7249fd6e4f6a 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 6d214df2d4d1..10fa06e8605d 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 3e0b50f3f2ec..1e7330429a2a 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 fadaec745f13..1ca24cf81a58 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 49b8bf9cda4c..bece6402c665 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/85] 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 b77ecacea713..88d8e8b9824b 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 17a32e4cc720..4045ad4bf4b6 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 10fa06e8605d..43fe9414eb7a 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 a78d8911cda3..52820c16521f 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 98ca5214ecf2..e9bf215dcdfc 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 f3850d14714b..a87a356d580b 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 8fb72c3da0b8..2ef3a429bac0 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 6fb7cbf18314..d84b674f88f4 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 b14eca288ad1..1ff8402974ce 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 ff1ca35dacad..218951b83610 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 9dc5acb5765a..855ae30d2198 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 3cc5c882e11d..7025c788be23 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 5f979a8a6994..a7e580acb851 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 2844643b45cf..5a46b9bbefe1 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 e021288345a5..6db24ac621fb 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 56af1685186d..7a9f6193d7af 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 91efa4a94691..4ab7f90c58e2 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 3b94b9533e88..2270005ee0dd 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 c9e24b319025..fc97686bb14d 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 de05f6af20e2..464435cb0a63 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 424fb44bf8c1..f886f9fbe3f5 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; From 17bb97c7cbebf42b274d337dfe4b64530ed8d3fe Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 26 Oct 2025 17:02:29 -0400 Subject: [PATCH 57/85] handle blocked attributes --- .../server/visitors/RegularElement.js | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js index df3e2fc37f1f..2ca16c4a2465 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js @@ -12,7 +12,8 @@ import { process_children, build_template, create_child_block, - PromiseOptimiser + PromiseOptimiser, + create_async_block } from './shared/utils.js'; /** @@ -202,13 +203,19 @@ export function RegularElement(node, context) { state.template.push(b.stmt(b.call('$.pop_element'))); } - if (optimiser.expressions.length > 0) { - context.state.template.push( - create_child_block( - b.block([optimiser.apply(), ...state.init, ...build_template(state.template)]), - true - ) + if (optimiser.is_async()) { + let statement = create_child_block( + b.block([optimiser.apply(), ...state.init, ...build_template(state.template)]), + true ); + + const blockers = optimiser.blockers(); + + if (blockers.elements.length > 0) { + statement = create_async_block(b.block([statement]), blockers, false, false); + } + + context.state.template.push(statement); } else { context.state.init.push(...state.init); context.state.template.push(...state.template); From f1715ac2e6f0adcfd5d11de1d6870bccfac3739d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 27 Oct 2025 14:09:20 +0100 Subject: [PATCH 58/85] WIP --- packages/svelte/src/compiler/phases/2-analyze/index.js | 5 +++-- 1 file changed, 3 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 73bcdfc61d53..9acc9fb99e6a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -695,10 +695,10 @@ export function analyze_component(root, source, options) { if (instance.has_await) { /** - * @param {ESTree.Expression} expression + * @param {ESTree.Node} expression * @param {Scope} scope * @param {Set} touched - * @param {Set} seen + * @param {Set} seen */ const touch = (expression, scope, touched, seen = new Set()) => { if (seen.has(expression)) return; @@ -708,6 +708,7 @@ export function analyze_component(root, source, options) { expression, { scope }, { + ImportDeclaration(node) {}, Identifier(node, context) { const parent = /** @type {ESTree.Node} */ (context.path.at(-1)); if (is_reference(node, parent)) { From 3ad47c324dabd933acfd93c9f48940d4e095dc54 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 27 Oct 2025 15:32:00 +0100 Subject: [PATCH 59/85] pre-transform --- .../src/compiler/phases/2-analyze/index.js | 309 ++++++++++-------- .../3-transform/client/transform-client.js | 2 +- .../client/visitors/ImportDeclaration.js | 1 + .../3-transform/client/visitors/Program.js | 2 +- .../3-transform/server/transform-server.js | 2 +- .../3-transform/server/visitors/Program.js | 2 +- .../3-transform/shared/transform-async.js | 164 +--------- .../svelte/src/compiler/phases/types.d.ts | 19 +- 8 files changed, 194 insertions(+), 307 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 9acc9fb99e6a..7b484f1adbf7 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -1,7 +1,7 @@ /** @import * as ESTree from 'estree' */ /** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */ /** @import { AnalysisState, Visitors } from './types' */ -/** @import { Analysis, AwaitedStatement, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */ +/** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */ import { walk } from 'zimmerframe'; import { parse } from '../1-parse/acorn.js'; import * as e from '../../errors.js'; @@ -549,7 +549,12 @@ export function analyze_component(root, source, options) { snippets: new Set(), async_deriveds: new Set(), pickled_awaits: new Set(), - awaited_statements: new Map() + instance_body: { + sync: [], + async: [], + declarations: [], + hoisted: [] + } }; if (!runes) { @@ -693,177 +698,191 @@ export function analyze_component(root, source, options) { e.legacy_rest_props_invalid(rest_props_refs[0].node); } - if (instance.has_await) { - /** - * @param {ESTree.Node} 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 }, - { - ImportDeclaration(node) {}, - 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} 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 }, + { + ImportDeclaration(node) {}, + 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 + * @param {Set} reads + * @param {Set} writes + */ + const trace_references = (node, reads, writes, seen = new Set()) => { + if (seen.has(node)) return; + seen.add(node); /** - * @param {ESTree.Node} node - * @param {Set} seen - * @param {Set} reads - * @param {Set} writes + * @param {ESTree.Pattern} node + * @param {Scope} scope */ - const trace_references = (node, reads, writes, seen = new Set()) => { - 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); - } + 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 }, - { - _(node, context) { - const scope = scopes.get(node); - if (scope) { - context.next({ scope }); - } else { - context.next(); - } - }, - AssignmentExpression(node, context) { - update(node.left, context.state.scope); - }, - UpdateExpression(node, context) { - update( - /** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node.argument), - context.state.scope - ); - }, - CallExpression(node, context) { - // 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)) { - const binding = context.state.scope.get(node.name); - if (binding) { - reads.add(binding); - } + walk( + node, + { scope: instance.scope }, + { + _(node, context) { + const scope = scopes.get(node); + if (scope) { + context.next({ scope }); + } else { + context.next(); + } + }, + AssignmentExpression(node, context) { + update(node.left, context.state.scope); + }, + UpdateExpression(node, context) { + update( + /** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node.argument), + context.state.scope + ); + }, + CallExpression(node, context) { + // 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)) { + const binding = context.state.scope.get(node.name); + if (binding) { + reads.add(binding); } } } - ); - }; + } + ); + }; - /** - * @param {ESTree.Statement | ESTree.VariableDeclarator | ESTree.FunctionDeclaration | ESTree.ClassDeclaration} node - */ - const push = (node) => { + let awaited = false; + + // TODO this should probably be attached to the scope? + var promises = b.id('$$promises'); + + /** + * @param {ESTree.Identifier} id + * @param {ESTree.Expression} blocker + */ + function push_declaration(id, blocker) { + analysis.instance_body.declarations.push(id); + + const binding = /** @type {Binding} */ (instance.scope.get(id.name)); + binding.blocker = blocker; + } + + for (let node of instance.ast.body) { + if (node.type === 'ImportDeclaration') { + analysis.instance_body.hoisted.push(node); + continue; + } + + if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportAllDeclaration') { + // these can't exist inside ` + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-head/B.svelte b/packages/svelte/tests/runtime-runes/samples/async-head/B.svelte new file mode 100644 index 000000000000..d725d5f03b59 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head/B.svelte @@ -0,0 +1,8 @@ + + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-head/_config.js b/packages/svelte/tests/runtime-runes/samples/async-head/_config.js new file mode 100644 index 000000000000..6fdf41b4340c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head/_config.js @@ -0,0 +1,23 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, window }) { + await tick(); + + const head = window.document.head; + + // we don't care about the order, but we want to ensure that the + // elements didn't clobber each other + for (let n of ['1', '2', '3']) { + const a = head.querySelector(`meta[name="a-${n}"]`); + assert.equal(a?.getAttribute('content'), n); + + const b1 = head.querySelector(`meta[name="b-${n}-1"]`); + assert.equal(b1?.getAttribute('content'), `${n}-1`); + + const b2 = head.querySelector(`meta[name="b-${n}-2"]`); + assert.equal(b2?.getAttribute('content'), `${n}-2`); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-head/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-head/main.svelte new file mode 100644 index 000000000000..7f2348937394 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head/main.svelte @@ -0,0 +1,11 @@ + + + + + + + + From e621f1ab0d8951188d03c0a268426cf784b72a5f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Oct 2025 02:26:09 +0100 Subject: [PATCH 65/85] fix tests --- packages/svelte/tests/hydration/test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/svelte/tests/hydration/test.ts b/packages/svelte/tests/hydration/test.ts index 70d5c5d0724f..ba13d2c61143 100644 --- a/packages/svelte/tests/hydration/test.ts +++ b/packages/svelte/tests/hydration/test.ts @@ -132,7 +132,11 @@ const { test, run } = suite(async (config, cwd) => { flushSync(); const normalize = (string: string) => - string.trim().replaceAll('\r\n', '\n').replaceAll('/>', '>'); + string + .trim() + .replaceAll('\r\n', '\n') + .replaceAll('/>', '>') + .replace(//g, ''); const expected = read(`${cwd}/_expected.html`) ?? rendered.html; assert.equal(normalize(target.innerHTML), normalize(expected)); From 2e56cd75753abc1fdfabbada9d27829d9eaa496a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Oct 2025 03:43:08 +0100 Subject: [PATCH 66/85] fix --- .../svelte/src/internal/client/dom/blocks/html.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js index d7190abc6668..aa06a0a42896 100644 --- a/packages/svelte/src/internal/client/dom/blocks/html.js +++ b/packages/svelte/src/internal/client/dom/blocks/html.js @@ -8,7 +8,7 @@ import * as w from '../../warnings.js'; import { hash, sanitize_location } from '../../../../utils.js'; import { DEV } from 'esm-env'; import { dev_current_component_function } from '../../context.js'; -import { get_first_child, get_next_sibling } from '../operations.js'; +import { create_text, get_first_child, get_next_sibling } from '../operations.js'; import { active_effect } from '../../runtime.js'; import { COMMENT_NODE } from '#client/constants'; @@ -86,7 +86,14 @@ export function html(node, get_value, svg = false, mathml = false, skip_warning } assign_nodes(hydrate_node, last); - anchor = set_hydrate_node(next); + + // in some cases the anchor could be in a fragment, and will be off-screen + // when we re-render. TODO figure out how this happens (appears to be + // forking-related) and come up with a regression test + anchor = create_text(); + next.before(anchor); + + set_hydrate_node(next); return; } From 2fa63eb61f8c0cf82dbd5d88713a6153a0d17ebd Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Oct 2025 04:42:06 +0100 Subject: [PATCH 67/85] delay resolve --- packages/svelte/src/internal/client/reactivity/batch.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index ab83050cd066..bbc05bb1ff87 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -196,6 +196,8 @@ export class Batch { flush_queued_effects(target.effects); previous_batch = null; + + this.#deferred?.resolve(); } batch_values = null; @@ -432,8 +434,6 @@ export class Batch { this.committed = true; batches.delete(this); - - this.#deferred?.resolve(); } /** From 761fb1e3256b8d409b6c11493b600ddcf4d8308c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Oct 2025 04:56:14 +0100 Subject: [PATCH 68/85] Revert "fix" This reverts commit 2e56cd75753abc1fdfabbada9d27829d9eaa496a. --- .../svelte/src/internal/client/dom/blocks/html.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js index aa06a0a42896..d7190abc6668 100644 --- a/packages/svelte/src/internal/client/dom/blocks/html.js +++ b/packages/svelte/src/internal/client/dom/blocks/html.js @@ -8,7 +8,7 @@ import * as w from '../../warnings.js'; import { hash, sanitize_location } from '../../../../utils.js'; import { DEV } from 'esm-env'; import { dev_current_component_function } from '../../context.js'; -import { create_text, get_first_child, get_next_sibling } from '../operations.js'; +import { get_first_child, get_next_sibling } from '../operations.js'; import { active_effect } from '../../runtime.js'; import { COMMENT_NODE } from '#client/constants'; @@ -86,14 +86,7 @@ export function html(node, get_value, svg = false, mathml = false, skip_warning } assign_nodes(hydrate_node, last); - - // in some cases the anchor could be in a fragment, and will be off-screen - // when we re-render. TODO figure out how this happens (appears to be - // forking-related) and come up with a regression test - anchor = create_text(); - next.before(anchor); - - set_hydrate_node(next); + anchor = set_hydrate_node(next); return; } From d029d9f9bbdfb83e5b1865e37e62ecbf57c19bde Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Oct 2025 06:30:55 +0100 Subject: [PATCH 69/85] add error --- packages/svelte/tests/runtime-legacy/shared.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 714454324237..a5f9389a51ee 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -12,6 +12,7 @@ import { raf } from '../animation-helpers.js'; import type { CompileOptions } from '#compiler'; import { suite_with_variants, type BaseTest } from '../suite.js'; import { clear } from '../../src/internal/client/reactivity/batch.js'; +import { hydrating } from '../../src/internal/client/dom/hydration.js'; type Assert = typeof import('vitest').assert & { htmlEqual(a: string, b: string, description?: string): void; @@ -533,6 +534,10 @@ async function run_test_variant( throw err; } } finally { + if (hydrating) { + // throw new Error('Hydration state was not cleared'); + } + config.after_test?.(); // Free up the microtask queue From a8708cacfa43f9dd37a4e1c4589ded3c98d457b9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Oct 2025 06:33:50 +0100 Subject: [PATCH 70/85] simplify/fix hydration restoration --- .../src/internal/client/dom/blocks/async.js | 7 +++-- .../src/internal/client/dom/blocks/if.js | 3 +- .../src/internal/client/reactivity/async.js | 31 ------------------- .../svelte/src/internal/client/warnings.js | 2 +- 4 files changed, 7 insertions(+), 36 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index d1c970fff7c8..995d4480fac3 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -50,10 +50,11 @@ export function async(node, blockers = [], expressions = [], fn) { } finally { boundary.update_pending_count(-1); batch.decrement(blocking); - } - if (was_hydrating) { - set_hydrating(false); + if (was_hydrating) { + set_hydrating(false); + set_hydrate_node(undefined); + } } }); } diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 7fa5ca464dd1..a80532a68f58 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -6,7 +6,8 @@ import { read_hydration_instruction, skip_nodes, set_hydrate_node, - set_hydrating + set_hydrating, + hydrate_node } from '../hydration.js'; import { block } from '../../reactivity/effects.js'; import { HYDRATION_START_ELSE } from '../../../../constants.js'; diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 47a3504668c8..e3459a2d064a 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -53,8 +53,6 @@ export function flatten(blockers, sync, async, fn) { var restore = capture(); - var was_hydrating = hydrating; - function run() { Promise.all(async.map((expression) => async_derived(expression))) .then((result) => { @@ -69,10 +67,6 @@ export function flatten(blockers, sync, async, fn) { } } - if (was_hydrating) { - set_hydrating(false); - } - batch?.deactivate(); unset_context(); }) @@ -108,12 +102,6 @@ export function capture() { var previous_component_context = component_context; var previous_batch = current_batch; - var was_hydrating = hydrating; - - if (was_hydrating) { - var previous_hydrate_node = hydrate_node; - } - if (DEV) { var previous_dev_stack = dev_stack; } @@ -124,11 +112,6 @@ export function capture() { set_component_context(previous_component_context); if (activate_batch) previous_batch?.activate(); - if (was_hydrating) { - set_hydrating(true); - set_hydrate_node(previous_hydrate_node); - } - if (DEV) { set_from_async_derived(null); set_dev_stack(previous_dev_stack); @@ -265,10 +248,6 @@ export async function async_body(anchor, fn) { invoke_error_boundary(error, active); } } finally { - if (was_hydrating) { - set_hydrating(false); - } - boundary.update_pending_count(-1); batch.decrement(blocking); @@ -294,8 +273,6 @@ export function run(thunks) { /** @type {null | { error: any }} */ var errored = null; - let was_hydrating = hydrating; - /** @param {any} error */ const handle_error = (error) => { errored = { error }; // wrap in object in case a promise rejects with a falsy value @@ -326,19 +303,11 @@ export function run(thunks) { } finally { // TODO do we need it here as well as below? unset_context(); - - if (was_hydrating) { - set_hydrating(false); - } } }) .catch(handle_error) .finally(() => { unset_context(); - - if (was_hydrating) { - set_hydrating(false); - } }); promises.push(promise); diff --git a/packages/svelte/src/internal/client/warnings.js b/packages/svelte/src/internal/client/warnings.js index 1081ef58618e..c49c7beef6c7 100644 --- a/packages/svelte/src/internal/client/warnings.js +++ b/packages/svelte/src/internal/client/warnings.js @@ -256,4 +256,4 @@ export function transition_slide_display(value) { } else { console.warn(`https://svelte.dev/e/transition_slide_display`); } -} \ No newline at end of file +} From 01d78536062e64d8ad34869a2a6b994daa55d79a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Oct 2025 06:39:05 +0100 Subject: [PATCH 71/85] fix --- packages/svelte/src/internal/client/dom/blocks/async.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index 995d4480fac3..c9eaf7e95fa4 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -48,13 +48,13 @@ export function async(node, blockers = [], expressions = [], fn) { fn(node, ...values); } finally { - boundary.update_pending_count(-1); - batch.decrement(blocking); - if (was_hydrating) { set_hydrating(false); set_hydrate_node(undefined); } + + boundary.update_pending_count(-1); + batch.decrement(blocking); } }); } From 584a52d2b5d276aa331d5fda757913ef9429a5d2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Oct 2025 07:38:00 +0100 Subject: [PATCH 72/85] use $state.eager mechanism for $effect.pending - way simpler and more robust --- .../3-transform/client/visitors/CallExpression.js | 2 +- .../src/internal/client/dom/blocks/boundary.js | 13 +++++-------- .../svelte/src/internal/client/reactivity/batch.js | 13 ------------- 3 files changed, 6 insertions(+), 22 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index 499708305905..e7468291a05c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -80,7 +80,7 @@ export function CallExpression(node, context) { ); case '$effect.pending': - return b.call('$.pending'); + return b.call('$.eager', b.thunk(b.call('$.pending'))); case '$inspect': case '$inspect().with': diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index febbc00898de..140e87633066 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -34,7 +34,7 @@ import { queue_micro_task } from '../task.js'; import * as e from '../../errors.js'; import * as w from '../../warnings.js'; import { DEV } from 'esm-env'; -import { Batch, effect_pending_updates } from '../../reactivity/batch.js'; +import { Batch } from '../../reactivity/batch.js'; import { internal_set, source } from '../../reactivity/sources.js'; import { tag } from '../../dev/tracing.js'; import { createSubscriber } from '../../../../reactivity/create-subscriber.js'; @@ -110,12 +110,6 @@ export class Boundary { */ #effect_pending = null; - #effect_pending_update = () => { - if (this.#effect_pending) { - internal_set(this.#effect_pending, this.#local_pending_count); - } - }; - #effect_pending_subscriber = createSubscriber(() => { this.#effect_pending = source(this.#local_pending_count); @@ -329,7 +323,10 @@ export class Boundary { this.#update_pending_count(d); this.#local_pending_count += d; - effect_pending_updates.add(this.#effect_pending_update); + + if (this.#effect_pending) { + internal_set(this.#effect_pending, this.#local_pending_count); + } } get_effect_pending() { diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index bbc05bb1ff87..9d61b6bbf92b 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -74,9 +74,6 @@ export let previous_batch = null; */ export let batch_values = null; -/** @type {Set<() => void>} */ -export let effect_pending_updates = new Set(); - /** @type {Effect[]} */ let queued_root_effects = []; @@ -324,16 +321,6 @@ export class Batch { } this.deactivate(); - - for (const update of effect_pending_updates) { - effect_pending_updates.delete(update); - update(); - - if (current_batch !== null) { - // only do one at a time - break; - } - } } discard() { From e4bec34b198a2f5f76e2190200c198f2d7c63c46 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Oct 2025 07:56:58 +0100 Subject: [PATCH 73/85] disable these warnings for now, too many false positives --- .../svelte/src/internal/client/runtime.js | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 49396d6febed..9f8822c3b76d 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -602,18 +602,19 @@ export function get(signal) { } if (DEV) { - if (current_async_effect) { - var tracking = (current_async_effect.f & REACTION_IS_UPDATING) !== 0; - var was_read = current_async_effect.deps?.includes(signal); - - if (!tracking && !untracking && !was_read) { - w.await_reactivity_loss(/** @type {string} */ (signal.label)); - - var trace = get_stack('traced at'); - // eslint-disable-next-line no-console - if (trace) console.warn(trace); - } - } + // TODO reinstate this, but make it actually work + // if (current_async_effect) { + // var tracking = (current_async_effect.f & REACTION_IS_UPDATING) !== 0; + // var was_read = current_async_effect.deps?.includes(signal); + + // if (!tracking && !untracking && !was_read) { + // w.await_reactivity_loss(/** @type {string} */ (signal.label)); + + // var trace = get_stack('traced at'); + // // eslint-disable-next-line no-console + // if (trace) console.warn(trace); + // } + // } recent_async_deriveds.delete(signal); From 7a1531fbefe0e7bb30a46d04058d1b5355c1251b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Oct 2025 08:03:10 +0100 Subject: [PATCH 74/85] fix --- .../phases/3-transform/server/visitors/CallExpression.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js index bba6511eec8d..8525fb636641 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js @@ -12,7 +12,14 @@ import { get_inspect_args } from '../../utils.js'; export function CallExpression(node, context) { const rune = get_rune(node, context.state.scope); - if (rune === '$host') { + if ( + rune === '$host' || + rune === '$effect' || + rune === '$effect.pre' || + rune === '$inspect.trace' + ) { + // we will only encounter `$effect` etc if they are top-level statements in the