From d92901edd1184e464bc020b362b78df658041571 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 1 Aug 2025 22:20:56 -0700 Subject: [PATCH 01/18] init --- .../src/compiler/phases/2-analyze/index.js | 14 +++++++++ .../src/compiler/phases/2-analyze/types.d.ts | 6 ++++ .../2-analyze/visitors/AwaitExpression.js | 3 ++ .../phases/2-analyze/visitors/Fragment.js | 29 +++++++++++++++++++ .../3-transform/client/transform-client.js | 1 + .../phases/3-transform/client/types.d.ts | 5 +++- .../phases/3-transform/client/utils.js | 16 ++++++++-- .../client/visitors/AwaitExpression.js | 6 +++- .../3-transform/client/visitors/ConstTag.js | 28 +++++++++++++----- .../3-transform/client/visitors/Fragment.js | 29 +++++++++++++++---- .../client/visitors/SnippetBlock.js | 18 +++++++----- .../svelte/src/compiler/types/template.d.ts | 5 +++- 12 files changed, 133 insertions(+), 27 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index cd44fd998aed..7c0943bf7c27 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -37,6 +37,7 @@ import { ExportNamedDeclaration } from './visitors/ExportNamedDeclaration.js'; import { ExportSpecifier } from './visitors/ExportSpecifier.js'; import { ExpressionStatement } from './visitors/ExpressionStatement.js'; import { ExpressionTag } from './visitors/ExpressionTag.js'; +import { Fragment } from './visitors/Fragment.js'; import { FunctionDeclaration } from './visitors/FunctionDeclaration.js'; import { FunctionExpression } from './visitors/FunctionExpression.js'; import { HtmlTag } from './visitors/HtmlTag.js'; @@ -156,6 +157,7 @@ const visitors = { ExportSpecifier, ExpressionStatement, ExpressionTag, + Fragment, FunctionDeclaration, FunctionExpression, HtmlTag, @@ -300,6 +302,10 @@ export function analyze_module(source, options) { function_depth: 0, has_props_rune: false, options: /** @type {ValidatedCompileOptions} */ (options), + fragment: { + has_await: false, + node: null + }, parent_element: null, reactive_statement: null }, @@ -688,6 +694,10 @@ export function analyze_component(root, source, options) { analysis, options, ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module', + fragment: { + has_await: false, + node: ast === template.ast ? template.ast : null + }, parent_element: null, has_props_rune: false, component_slots: new Set(), @@ -753,6 +763,10 @@ export function analyze_component(root, source, options) { scopes, analysis, options, + fragment: { + has_await: false, + node: ast === template.ast ? template.ast : null + }, parent_element: null, has_props_rune: false, ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module', diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index 080239bac063..7f7dfa3c1474 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -8,6 +8,7 @@ export interface AnalysisState { analysis: ComponentAnalysis; options: ValidatedCompileOptions; ast_type: 'instance' | 'template' | 'module'; + fragment: FragmentAnalysis; /** * Tag name of the parent element. `null` if the parent is `svelte:element`, `#snippet`, a component or the root. * Parent doesn't necessarily mean direct path predecessor because there could be `#each`, `#if` etc in-between. @@ -28,6 +29,11 @@ export interface AnalysisState { reactive_statement: null | ReactiveStatement; } +export interface FragmentAnalysis { + has_await: boolean; + node: AST.Fragment | null; +} + export type Context = import('zimmerframe').Context< AST.SvelteNode, State diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index af7d0307e9dc..6e9052a0286b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -11,6 +11,9 @@ export function AwaitExpression(node, context) { if (context.state.expression) { context.state.expression.has_await = true; + if (context.state.fragment.node) { + context.state.fragment.has_await = true; + } suspend = true; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js new file mode 100644 index 000000000000..4916c8910ea5 --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js @@ -0,0 +1,29 @@ +/** @import { AST } from '#compiler' */ +/** @import { Context } from '../types.js' */ + +/** + * @param {AST.Fragment} node + * @param {Context} context + */ +export function Fragment(node, context) { + const parent = /** @type {AST.TemplateNode} */ (context.path.at(-1)); + if ( + !parent || + parent.type === 'Component' || + parent.type === 'Root' || + parent.type === 'IfBlock' || + parent.type === 'KeyBlock' || + parent.type === 'EachBlock' || + parent.type === 'SnippetBlock' || + parent.type === 'AwaitBlock' + ) { + const fragment_metadata = { + has_await: false, + node + }; + context.next({ ...context.state, fragment: fragment_metadata }); + node.metadata.has_await = fragment_metadata.has_await; + } else { + context.next(); + } +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index a56aca9c5f0b..560d6c67b743 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -170,6 +170,7 @@ export function client_component(analysis, options) { // these are set inside the `Fragment` visitor, and cannot be used until then init: /** @type {any} */ (null), + consts: /** @type {any} */ (null), update: /** @type {any} */ (null), after_update: /** @type {any} */ (null), template: /** @type {any} */ (null), diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index e691be169b55..30ad2c29e852 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -6,7 +6,8 @@ import type { Expression, AssignmentExpression, UpdateExpression, - VariableDeclaration + VariableDeclaration, + Declaration } from 'estree'; import type { AST, Namespace, ValidatedCompileOptions } from '#compiler'; import type { TransformState } from '../types.js'; @@ -57,6 +58,8 @@ export interface ComponentClientTransformState extends ClientTransformState { readonly update: Statement[]; /** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */ readonly after_update: Statement[]; + /** Transformed `{#const }` declarations */ + readonly consts: Statement[]; /** Memoized expressions */ readonly memoizer: Memoizer; /** The HTML template string */ diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index 6d9dac8a33d3..ff3d439d59f4 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -290,7 +290,19 @@ export function should_proxy(node, scope) { * Svelte legacy mode should use safe equals in most places, runes mode shouldn't * @param {ComponentClientTransformState} state * @param {Expression} arg + * @param {boolean} [async] */ -export function create_derived(state, arg) { - return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg); +export function create_derived(state, arg, async = false) { + if (async) { + return b.call( + b.await( + b.call( + '$.save', + b.call('$.async_derived', arg.type === 'ArrowFunctionExpression' ? b.async(arg) : arg) + ) + ) + ); + } else { + return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg); + } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js index 803d317ad40e..7acd5aaaae12 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js @@ -15,7 +15,11 @@ export function AwaitExpression(node, context) { // preserve context for // a) top-level await and // b) awaits that precede other expressions in template or `$derived(...)` - if (tla || (is_reactive_expression(context) && !is_last_evaluated_expression(context, node))) { + if ( + tla || + (is_reactive_expression(context) && + (!is_last_evaluated_expression(context, node) || context.path.at(-1)?.type === 'ConstTag')) + ) { return b.call(b.await(b.call('$.save', argument))); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js index 34acdd6bb975..d5c5b2ea41fe 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js @@ -16,21 +16,29 @@ export function ConstTag(node, context) { const declaration = node.declaration.declarations[0]; // TODO we can almost certainly share some code with $derived(...) if (declaration.id.type === 'Identifier') { - const init = build_expression(context, declaration.init, node.metadata.expression); - let expression = create_derived(context.state, b.thunk(init)); + const init = build_expression( + { ...context, state: { ...context.state, in_derived: true } }, + declaration.init, + node.metadata.expression + ); + let expression = create_derived( + context.state, + b.thunk(init), + node.metadata.expression.has_await + ); if (dev) { expression = b.call('$.tag', expression, b.literal(declaration.id.name)); } - context.state.init.push(b.const(declaration.id, expression)); + context.state.consts.push(b.const(declaration.id, expression)); context.state.transform[declaration.id.name] = { read: get_value }; // we need to eagerly evaluate the expression in order to hit any // 'Cannot access x before initialization' errors if (dev) { - context.state.init.push(b.stmt(b.call('$.get', declaration.id))); + context.state.consts.push(b.stmt(b.call('$.get', declaration.id))); } } else { const identifiers = extract_identifiers(declaration.id); @@ -44,7 +52,11 @@ export function ConstTag(node, context) { delete transform[node.name]; } - const child_state = { ...context.state, transform }; + const child_state = /** @type {ComponentContext['state']} */ ({ + ...context.state, + transform, + in_derived: true + }); // TODO optimise the simple `{ x } = y` case — we can just return `y` // instead of destructuring it only to return a new object @@ -61,18 +73,18 @@ export function ConstTag(node, context) { ]) ); - let expression = create_derived(context.state, fn); + let expression = create_derived(context.state, fn, node.metadata.expression.has_await); if (dev) { expression = b.call('$.tag', expression, b.literal('[@const]')); } - context.state.init.push(b.const(tmp, expression)); + context.state.consts.push(b.const(tmp, expression)); // we need to eagerly evaluate the expression in order to hit any // 'Cannot access x before initialization' errors if (dev) { - context.state.init.push(b.stmt(b.call('$.get', tmp))); + context.state.consts.push(b.stmt(b.call('$.get', tmp))); } for (const node of identifiers) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index 0b10c02ffbe1..abda1c164480 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -48,8 +48,10 @@ export function Fragment(node, context) { const is_single_child_not_needing_template = trimmed.length === 1 && (trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement'); + const has_await = context.state.init !== null && (node.metadata.has_await || false); const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent + const unsuspend = b.id('$$unsuspend'); /** @type {Statement[]} */ const body = []; @@ -61,6 +63,7 @@ export function Fragment(node, context) { const state = { ...context.state, init: [], + consts: [], update: [], after_update: [], memoizer: new Memoizer(), @@ -78,7 +81,7 @@ export function Fragment(node, context) { if (is_text_first) { // skip over inserted comment - body.push(b.stmt(b.call('$.next'))); + state.init.unshift(b.stmt(b.call('$.next'))); } if (is_single_element) { @@ -96,13 +99,13 @@ export function Fragment(node, context) { const template = transform_template(state, namespace, flags); state.hoisted.push(b.var(template_name, template)); - body.push(b.var(id, b.call(template_name))); + state.init.unshift(b.var(id, b.call(template_name))); close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); } else if (is_single_child_not_needing_template) { context.visit(trimmed[0], state); } else if (trimmed.length === 1 && trimmed[0].type === 'Text') { const id = b.id(context.state.scope.generate('text')); - body.push(b.var(id, b.call('$.text', b.literal(trimmed[0].data)))); + state.init.unshift(b.var(id, b.call('$.text', b.literal(trimmed[0].data)))); close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); } else if (trimmed.length > 0) { const id = b.id(context.state.scope.generate('fragment')); @@ -120,7 +123,7 @@ export function Fragment(node, context) { state }); - body.push(b.var(id, b.call('$.text'))); + state.init.unshift(b.var(id, b.call('$.text'))); close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); } else { if (is_standalone) { @@ -140,12 +143,12 @@ export function Fragment(node, context) { if (state.template.nodes.length === 1 && state.template.nodes[0].type === 'comment') { // special case — we can use `$.comment` instead of creating a unique template - body.push(b.var(id, b.call('$.comment'))); + state.init.unshift(b.var(id, b.call('$.comment'))); } else { const template = transform_template(state, namespace, flags); state.hoisted.push(b.var(template_name, template)); - body.push(b.var(id, b.call(template_name))); + state.init.unshift(b.var(id, b.call(template_name))); } close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); @@ -153,6 +156,16 @@ export function Fragment(node, context) { } } + if (has_await) { + body.push(b.var(unsuspend, b.call('$.suspend'))); + } + + body.push(...state.consts); + + if (has_await) { + body.push(b.if(b.call('$.aborted'), b.return())); + } + body.push(...state.init); if (state.update.length > 0) { @@ -168,5 +181,9 @@ export function Fragment(node, context) { body.push(close); } + if (has_await) { + body.push(b.stmt(b.call(unsuspend))); + } + return b.block(body); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js index 203cf62b3792..98894780e1fa 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js @@ -14,6 +14,7 @@ export function SnippetBlock(node, context) { // TODO hoist where possible /** @type {(Identifier | AssignmentPattern)[]} */ const args = [b.id('$$anchor')]; + const has_await = node.body.metadata.has_await || false; /** @type {BlockStatement} */ let body; @@ -21,10 +22,6 @@ export function SnippetBlock(node, context) { /** @type {Statement[]} */ const declarations = []; - if (dev) { - declarations.push(b.stmt(b.call('$.validate_snippet_args', b.spread(b.id('arguments'))))); - } - const transform = { ...context.state.transform }; const child_state = { ...context.state, transform }; @@ -72,16 +69,21 @@ export function SnippetBlock(node, context) { } } } - + const block = /** @type {BlockStatement} */ (context.visit(node.body, child_state)).body; body = b.block([ + dev ? b.stmt(b.call('$.validate_snippet_args', b.spread(b.id('arguments')))) : b.empty, ...declarations, - .../** @type {BlockStatement} */ (context.visit(node.body, child_state)).body + ...block ]); // in dev we use a FunctionExpression (not arrow function) so we can use `arguments` let snippet = dev - ? b.call('$.wrap_snippet', b.id(context.state.analysis.name), b.function(null, args, body)) - : b.arrow(args, body); + ? b.call( + '$.wrap_snippet', + b.id(context.state.analysis.name), + has_await ? b.async(b.function(null, args, body)) : b.function(null, args, body) + ) + : b.arrow(args, body, has_await); const declaration = b.const(node.expression, snippet); diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 058a1a8e66c3..8cb3657a5aeb 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -19,6 +19,9 @@ import type { } from 'estree'; import type { Scope } from '../phases/scope'; import type { _CSS } from './css'; +import type { FragmentAnalysis } from '../phases/2-analyze/types'; + +type FragmentMetadata = Omit; /** * - `html` — the default, for e.g. `
` or `` @@ -45,7 +48,7 @@ export namespace AST { type: 'Fragment'; nodes: Array; /** @internal */ - metadata: { + metadata: Partial & { /** * Fragments declare their own scopes. A transparent fragment is one whose scope * is not represented by a scope in the resulting JavaScript (e.g. an element scope), From 7fa57b1b0e370d94b23d4496192c04aa08110f9d Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 1 Aug 2025 22:47:45 -0700 Subject: [PATCH 02/18] fix --- .../compiler/phases/2-analyze/visitors/AwaitExpression.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index 6e9052a0286b..1f1c8ca6714d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -11,7 +11,10 @@ export function AwaitExpression(node, context) { if (context.state.expression) { context.state.expression.has_await = true; - if (context.state.fragment.node) { + if ( + context.state.fragment.node && + !context.path.find((node) => node.type === 'ExpressionTag') + ) { context.state.fragment.has_await = true; } suspend = true; From 5787224971eb8cc7b3a071caa8a986e477d1e4ea Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 1 Aug 2025 22:49:21 -0700 Subject: [PATCH 03/18] fix --- .../src/compiler/phases/2-analyze/visitors/AwaitExpression.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index 1f1c8ca6714d..bf18358106d8 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -13,7 +13,8 @@ export function AwaitExpression(node, context) { context.state.expression.has_await = true; if ( context.state.fragment.node && - !context.path.find((node) => node.type === 'ExpressionTag') + // TODO there's probably a better way to do this + context.path.find((node) => node.type === 'ConstTag') ) { context.state.fragment.has_await = true; } From 4d677151ab7f08e4f718f983f7d706432dc31f59 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 1 Aug 2025 23:13:43 -0700 Subject: [PATCH 04/18] maybe this works? --- .../phases/2-analyze/visitors/Fragment.js | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js index 4916c8910ea5..4a2d621629af 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js @@ -7,23 +7,23 @@ */ export function Fragment(node, context) { const parent = /** @type {AST.TemplateNode} */ (context.path.at(-1)); - if ( - !parent || - parent.type === 'Component' || - parent.type === 'Root' || - parent.type === 'IfBlock' || - parent.type === 'KeyBlock' || - parent.type === 'EachBlock' || - parent.type === 'SnippetBlock' || - parent.type === 'AwaitBlock' - ) { + // if ( + // !parent || + // parent.type === 'Component' || + // parent.type === 'Root' || + // parent.type === 'IfBlock' || + // parent.type === 'KeyBlock' || + // parent.type === 'EachBlock' || + // parent.type === 'SnippetBlock' || + // parent.type === 'AwaitBlock' + // ) { const fragment_metadata = { has_await: false, node }; context.next({ ...context.state, fragment: fragment_metadata }); node.metadata.has_await = fragment_metadata.has_await; - } else { - context.next(); - } + // } else { + // context.next(); + // } } From ebca33930cf8c42fe6892736dde8f098e22105d8 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 1 Aug 2025 23:25:12 -0700 Subject: [PATCH 05/18] fix, minor cleanup --- .../phases/2-analyze/visitors/Fragment.js | 26 +++++-------------- .../client/visitors/SvelteBoundary.js | 2 +- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js index 4a2d621629af..f3442bad49ef 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js @@ -6,24 +6,10 @@ * @param {Context} context */ export function Fragment(node, context) { - const parent = /** @type {AST.TemplateNode} */ (context.path.at(-1)); - // if ( - // !parent || - // parent.type === 'Component' || - // parent.type === 'Root' || - // parent.type === 'IfBlock' || - // parent.type === 'KeyBlock' || - // parent.type === 'EachBlock' || - // parent.type === 'SnippetBlock' || - // parent.type === 'AwaitBlock' - // ) { - const fragment_metadata = { - has_await: false, - node - }; - context.next({ ...context.state, fragment: fragment_metadata }); - node.metadata.has_await = fragment_metadata.has_await; - // } else { - // context.next(); - // } + const fragment_metadata = { + has_await: false, + node + }; + context.next({ ...context.state, fragment: fragment_metadata }); + node.metadata.has_await = fragment_metadata.has_await; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js index d37b990440c7..70df0223557d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js @@ -43,7 +43,7 @@ export function SvelteBoundary(node, context) { // to resolve this we cheat: we duplicate const tags inside snippets for (const child of node.fragment.nodes) { if (child.type === 'ConstTag') { - context.visit(child, { ...context.state, init: const_tags }); + context.visit(child, { ...context.state, consts: const_tags }); } } From d12ee9bf4fd7d3bdb7e12d46e5c95c4dca6e3e6f Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 2 Aug 2025 16:02:27 -0700 Subject: [PATCH 06/18] maybe this'll fix hydration issues? --- .../compiler/phases/3-transform/client/visitors/Fragment.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index abda1c164480..fbad556e09b5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -99,13 +99,13 @@ export function Fragment(node, context) { const template = transform_template(state, namespace, flags); state.hoisted.push(b.var(template_name, template)); - state.init.unshift(b.var(id, b.call(template_name))); + state.init.push(b.var(id, b.call(template_name))); close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); } else if (is_single_child_not_needing_template) { context.visit(trimmed[0], state); } else if (trimmed.length === 1 && trimmed[0].type === 'Text') { const id = b.id(context.state.scope.generate('text')); - state.init.unshift(b.var(id, b.call('$.text', b.literal(trimmed[0].data)))); + state.init.push(b.var(id, b.call('$.text', b.literal(trimmed[0].data)))); close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); } else if (trimmed.length > 0) { const id = b.id(context.state.scope.generate('fragment')); @@ -123,7 +123,7 @@ export function Fragment(node, context) { state }); - state.init.unshift(b.var(id, b.call('$.text'))); + state.init.push(b.var(id, b.call('$.text'))); close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); } else { if (is_standalone) { From 58842f5795dde103be99bd1fb0451871a3c280b5 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 2 Aug 2025 16:09:54 -0700 Subject: [PATCH 07/18] apparently not, maybe this'll do it --- .../3-transform/client/visitors/Fragment.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index fbad556e09b5..c7c576101e5d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -79,11 +79,6 @@ export function Fragment(node, context) { context.visit(node, state); } - if (is_text_first) { - // skip over inserted comment - state.init.unshift(b.stmt(b.call('$.next'))); - } - if (is_single_element) { const element = /** @type {AST.RegularElement} */ (trimmed[0]); @@ -99,13 +94,13 @@ export function Fragment(node, context) { const template = transform_template(state, namespace, flags); state.hoisted.push(b.var(template_name, template)); - state.init.push(b.var(id, b.call(template_name))); + state.init.unshift(b.var(id, b.call(template_name))); close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); } else if (is_single_child_not_needing_template) { context.visit(trimmed[0], state); } else if (trimmed.length === 1 && trimmed[0].type === 'Text') { const id = b.id(context.state.scope.generate('text')); - state.init.push(b.var(id, b.call('$.text', b.literal(trimmed[0].data)))); + state.init.unshift(b.var(id, b.call('$.text', b.literal(trimmed[0].data)))); close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); } else if (trimmed.length > 0) { const id = b.id(context.state.scope.generate('fragment')); @@ -123,7 +118,7 @@ export function Fragment(node, context) { state }); - state.init.push(b.var(id, b.call('$.text'))); + state.init.unshift(b.var(id, b.call('$.text'))); close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); } else { if (is_standalone) { @@ -166,6 +161,11 @@ export function Fragment(node, context) { body.push(b.if(b.call('$.aborted'), b.return())); } + if (is_text_first) { + // skip over inserted comment + body.push(b.stmt(b.call('$.next'))); + } + body.push(...state.init); if (state.update.length > 0) { From 7b19d12d34de72cb78a8ea4054b98e7eb8e93694 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 2 Aug 2025 16:39:18 -0700 Subject: [PATCH 08/18] add test, changeset --- .changeset/brave-baboons-teach.md | 5 ++++ .../samples/async-const/_config.js | 16 +++++++++++++ .../samples/async-const/main.svelte | 23 +++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 .changeset/brave-baboons-teach.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-const/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-const/main.svelte diff --git a/.changeset/brave-baboons-teach.md b/.changeset/brave-baboons-teach.md new file mode 100644 index 000000000000..ca761018437c --- /dev/null +++ b/.changeset/brave-baboons-teach.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: async fragments diff --git a/packages/svelte/tests/runtime-runes/samples/async-const/_config.js b/packages/svelte/tests/runtime-runes/samples/async-const/_config.js new file mode 100644 index 000000000000..9319bafeb023 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-const/_config.js @@ -0,0 +1,16 @@ +import { test } from '../../test'; + +export default test({ + html: `

Loading...

`, + + async test({ assert, target }) { + await new Promise((resolve) => setTimeout(resolve, 100)); + assert.htmlEqual( + target.innerHTML, + ` +

Hello, world!

+ + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte new file mode 100644 index 000000000000..bbcf82739fbf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte @@ -0,0 +1,23 @@ + + + {#snippet pending()} +

Loading...

+ {/snippet} + {#snippet greet()} + {@const greeting = await wait(`Hello, ${name}!`, 50)} +

{greeting}

+ + {/snippet} + {@render greet()} +
\ No newline at end of file From 2522b98a8e567f078b997d19853938d783534911 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 2 Aug 2025 17:01:16 -0700 Subject: [PATCH 09/18] minor tweak --- .../svelte/src/compiler/phases/3-transform/client/types.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index 30ad2c29e852..59c024dfb78b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -58,7 +58,7 @@ export interface ComponentClientTransformState extends ClientTransformState { readonly update: Statement[]; /** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */ readonly after_update: Statement[]; - /** Transformed `{#const }` declarations */ + /** Transformed `{@const }` declarations */ readonly consts: Statement[]; /** Memoized expressions */ readonly memoizer: Memoizer; From 1fd17a2ec3347129ebe3ac65fff1421d322d64d3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 5 Aug 2025 21:30:00 -0400 Subject: [PATCH 10/18] tabs --- .../samples/async-const/_config.js | 4 +- .../samples/async-const/main.svelte | 40 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/svelte/tests/runtime-runes/samples/async-const/_config.js b/packages/svelte/tests/runtime-runes/samples/async-const/_config.js index 9319bafeb023..f9ce32506494 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-const/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-const/_config.js @@ -8,8 +8,8 @@ export default test({ assert.htmlEqual( target.innerHTML, ` -

Hello, world!

- +

Hello, world!

+ ` ); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte index bbcf82739fbf..3c13320fa61b 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte @@ -1,23 +1,23 @@ - {#snippet pending()} -

Loading...

- {/snippet} - {#snippet greet()} - {@const greeting = await wait(`Hello, ${name}!`, 50)} -

{greeting}

- - {/snippet} - {@render greet()} -
\ No newline at end of file + {#snippet pending()} +

Loading...

+ {/snippet} + {#snippet greet()} + {@const greeting = await wait(`Hello, ${name}!`, 50)} +

{greeting}

+ + {/snippet} + {@render greet()} + From 201bf3704bc16cf7fb73c539126d586974f0e2ce Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 5 Aug 2025 21:32:02 -0400 Subject: [PATCH 11/18] avoid timeouts in tests, they add up quickly --- .../runtime-runes/samples/async-const/_config.js | 12 ++++-------- .../runtime-runes/samples/async-const/main.svelte | 15 ++++----------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/packages/svelte/tests/runtime-runes/samples/async-const/_config.js b/packages/svelte/tests/runtime-runes/samples/async-const/_config.js index f9ce32506494..084d9c3874ef 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-const/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-const/_config.js @@ -1,16 +1,12 @@ +import { tick } from 'svelte'; import { test } from '../../test'; export default test({ html: `

Loading...

`, async test({ assert, target }) { - await new Promise((resolve) => setTimeout(resolve, 100)); - assert.htmlEqual( - target.innerHTML, - ` -

Hello, world!

- - ` - ); + await tick(); + + assert.htmlEqual(target.innerHTML, `

Hello, world!

`); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte index 3c13320fa61b..9321bd792980 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte @@ -1,23 +1,16 @@ + {#snippet pending()}

Loading...

{/snippet} + {#snippet greet()} - {@const greeting = await wait(`Hello, ${name}!`, 50)} + {@const greeting = await `Hello, ${name}!`}

{greeting}

- {/snippet} + {@render greet()}
From f3daef7947c2c720d9f626cf04d5fbca6262ba6e Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Tue, 5 Aug 2025 19:15:43 -0700 Subject: [PATCH 12/18] tweak --- .../svelte/src/compiler/phases/2-analyze/index.js | 15 +++------------ .../src/compiler/phases/2-analyze/types.d.ts | 7 +------ .../phases/2-analyze/visitors/AwaitExpression.js | 4 ++-- .../phases/2-analyze/visitors/Fragment.js | 8 ++------ packages/svelte/src/compiler/types/template.d.ts | 6 ++---- 5 files changed, 10 insertions(+), 30 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 7c0943bf7c27..a64f7048e443 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -302,10 +302,7 @@ export function analyze_module(source, options) { function_depth: 0, has_props_rune: false, options: /** @type {ValidatedCompileOptions} */ (options), - fragment: { - has_await: false, - node: null - }, + fragment: null, parent_element: null, reactive_statement: null }, @@ -694,10 +691,7 @@ export function analyze_component(root, source, options) { analysis, options, ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module', - fragment: { - has_await: false, - node: ast === template.ast ? template.ast : null - }, + fragment: ast === template.ast ? ast : null, parent_element: null, has_props_rune: false, component_slots: new Set(), @@ -763,10 +757,7 @@ export function analyze_component(root, source, options) { scopes, analysis, options, - fragment: { - has_await: false, - node: ast === template.ast ? template.ast : null - }, + fragment: ast === template.ast ? ast : null, parent_element: null, has_props_rune: false, ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module', diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index 7f7dfa3c1474..2d99a2e155f6 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -8,7 +8,7 @@ export interface AnalysisState { analysis: ComponentAnalysis; options: ValidatedCompileOptions; ast_type: 'instance' | 'template' | 'module'; - fragment: FragmentAnalysis; + fragment: AST.Fragment | null; /** * Tag name of the parent element. `null` if the parent is `svelte:element`, `#snippet`, a component or the root. * Parent doesn't necessarily mean direct path predecessor because there could be `#each`, `#if` etc in-between. @@ -29,11 +29,6 @@ export interface AnalysisState { reactive_statement: null | ReactiveStatement; } -export interface FragmentAnalysis { - has_await: boolean; - node: AST.Fragment | null; -} - export type Context = import('zimmerframe').Context< AST.SvelteNode, State diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index bf18358106d8..4e9d0e93da44 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -12,11 +12,11 @@ export function AwaitExpression(node, context) { if (context.state.expression) { context.state.expression.has_await = true; if ( - context.state.fragment.node && + context.state.fragment && // TODO there's probably a better way to do this context.path.find((node) => node.type === 'ConstTag') ) { - context.state.fragment.has_await = true; + context.state.fragment.metadata.has_await = true; } suspend = true; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js index f3442bad49ef..d6e155f2f135 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js @@ -6,10 +6,6 @@ * @param {Context} context */ export function Fragment(node, context) { - const fragment_metadata = { - has_await: false, - node - }; - context.next({ ...context.state, fragment: fragment_metadata }); - node.metadata.has_await = fragment_metadata.has_await; + node.metadata.has_await = false; + context.next({ ...context.state, fragment: node }); } diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 8cb3657a5aeb..060df2dcb2a4 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -19,9 +19,6 @@ import type { } from 'estree'; import type { Scope } from '../phases/scope'; import type { _CSS } from './css'; -import type { FragmentAnalysis } from '../phases/2-analyze/types'; - -type FragmentMetadata = Omit; /** * - `html` — the default, for e.g. `
` or `` @@ -48,7 +45,7 @@ export namespace AST { type: 'Fragment'; nodes: Array; /** @internal */ - metadata: Partial & { + metadata: { /** * Fragments declare their own scopes. A transparent fragment is one whose scope * is not represented by a scope in the resulting JavaScript (e.g. an element scope), @@ -59,6 +56,7 @@ export namespace AST { * Whether or not we need to traverse into the fragment during mount/hydrate */ dynamic: boolean; + has_await: boolean; }; } From b1cc101c085327b412ac10a043737c30e9dc474f Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Tue, 5 Aug 2025 19:21:41 -0700 Subject: [PATCH 13/18] fix --- packages/svelte/src/compiler/phases/1-parse/utils/create.js | 3 ++- .../svelte/src/compiler/phases/2-analyze/visitors/Fragment.js | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/create.js b/packages/svelte/src/compiler/phases/1-parse/utils/create.js index 6030f1bd7bff..2fba918f20ee 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/create.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/create.js @@ -10,7 +10,8 @@ export function create_fragment(transparent = false) { nodes: [], metadata: { transparent, - dynamic: false + dynamic: false, + has_await: false } }; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js index d6e155f2f135..02d780dc0dc7 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js @@ -6,6 +6,5 @@ * @param {Context} context */ export function Fragment(node, context) { - node.metadata.has_await = false; context.next({ ...context.state, fragment: node }); } From e57f09ffe91e1b22a5c3c4147d3b07ad62935f67 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 6 Aug 2025 09:47:05 -0400 Subject: [PATCH 14/18] some is more 'correct' than find --- .../src/compiler/phases/2-analyze/visitors/AwaitExpression.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index 4e9d0e93da44..b2f59b849b42 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -11,13 +11,15 @@ export function AwaitExpression(node, context) { if (context.state.expression) { context.state.expression.has_await = true; + if ( context.state.fragment && // TODO there's probably a better way to do this - context.path.find((node) => node.type === 'ConstTag') + context.path.some((node) => node.type === 'ConstTag') ) { context.state.fragment.metadata.has_await = true; } + suspend = true; } From 9587530a32746e8a85d42b0758a0c85cd66d42f2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 6 Aug 2025 10:32:31 -0400 Subject: [PATCH 15/18] chore: remove `b.async` --- .../compiler/phases/3-transform/shared/assignments.js | 9 +++++---- packages/svelte/src/compiler/utils/builders.js | 9 --------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js b/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js index 175b44f4fe29..08537577751a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js +++ b/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js @@ -65,13 +65,14 @@ export function visit_assignment_expression(node, context, build_assignment) { statements.push(b.return(rhs)); } - const iife = b.arrow([rhs], b.block(statements)); - - const iife_is_async = + const async = is_expression_async(value) || assignments.some((assignment) => is_expression_async(assignment)); - return iife_is_async ? b.await(b.call(b.async(iife), value)) : b.call(iife, value); + const iife = b.arrow([rhs], b.block(statements), async); + const call = b.call(iife, value); + + return async ? b.await(call) : call; } const sequence = b.sequence(assignments); diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index 81c4e6b8e0c5..defdb1642e1d 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -56,15 +56,6 @@ export function assignment(operator, left, right) { return { type: 'AssignmentExpression', operator, left, right }; } -/** - * @template T - * @param {T & ESTree.BaseFunction} func - * @returns {T & ESTree.BaseFunction} - */ -export function async(func) { - return { ...func, async: true }; -} - /** * @param {ESTree.Expression} argument * @returns {ESTree.AwaitExpression} From 529571d18489d895b4e044c8f0d20c15481a1376 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 6 Aug 2025 12:06:26 -0400 Subject: [PATCH 16/18] simplify --- .../phases/3-transform/client/utils.js | 19 +++++++---------- .../3-transform/client/visitors/AwaitBlock.js | 4 ++-- .../3-transform/client/visitors/ConstTag.js | 21 +++++++------------ .../client/visitors/LetDirective.js | 5 +---- .../client/visitors/SnippetBlock.js | 2 +- .../svelte/src/compiler/utils/builders.js | 4 ++-- 6 files changed, 21 insertions(+), 34 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index ff3d439d59f4..19a4342b5eb1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -1,4 +1,4 @@ -/** @import { ArrowFunctionExpression, AssignmentExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Node, Pattern, UpdateExpression } from 'estree' */ +/** @import { ArrowFunctionExpression, AssignmentExpression, BlockStatement, Expression, FunctionDeclaration, FunctionExpression, Identifier, Node, Pattern, UpdateExpression } from 'estree' */ /** @import { Binding } from '#compiler' */ /** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */ /** @import { Analysis } from '../../types.js' */ @@ -289,20 +289,15 @@ export function should_proxy(node, scope) { /** * Svelte legacy mode should use safe equals in most places, runes mode shouldn't * @param {ComponentClientTransformState} state - * @param {Expression} arg + * @param {Expression | BlockStatement} expression * @param {boolean} [async] */ -export function create_derived(state, arg, async = false) { +export function create_derived(state, expression, async = false) { + const thunk = b.thunk(expression, async); + if (async) { - return b.call( - b.await( - b.call( - '$.save', - b.call('$.async_derived', arg.type === 'ArrowFunctionExpression' ? b.async(arg) : arg) - ) - ) - ); + return b.call(b.await(b.call('$.save', b.call('$.async_derived', thunk)))); } else { - return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg); + return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', thunk); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js index 4246091bcf88..e2e8e93f768c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js @@ -96,13 +96,13 @@ function create_derived_block_argument(node, context) { b.return(b.object(identifiers.map((identifier) => b.prop('init', identifier, identifier)))) ]); - const declarations = [b.var(value, create_derived(context.state, b.thunk(block)))]; + const declarations = [b.var(value, create_derived(context.state, block))]; for (const id of identifiers) { context.state.transform[id.name] = { read: get_value }; declarations.push( - b.var(id, create_derived(context.state, b.thunk(b.member(b.call('$.get', value), id)))) + b.var(id, create_derived(context.state, b.member(b.call('$.get', value), id))) ); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js index d5c5b2ea41fe..b550dae890db 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js @@ -21,11 +21,8 @@ export function ConstTag(node, context) { declaration.init, node.metadata.expression ); - let expression = create_derived( - context.state, - b.thunk(init), - node.metadata.expression.has_await - ); + + let expression = create_derived(context.state, init, node.metadata.expression.has_await); if (dev) { expression = b.call('$.tag', expression, b.literal(declaration.id.name)); @@ -65,15 +62,13 @@ export function ConstTag(node, context) { declaration.init, node.metadata.expression ); - const fn = b.arrow( - [], - b.block([ - b.const(/** @type {Pattern} */ (context.visit(declaration.id, child_state)), init), - b.return(b.object(identifiers.map((node) => b.prop('init', node, node)))) - ]) - ); - let expression = create_derived(context.state, fn, node.metadata.expression.has_await); + const block = b.block([ + b.const(/** @type {Pattern} */ (context.visit(declaration.id, child_state)), init), + b.return(b.object(identifiers.map((node) => b.prop('init', node, node)))) + ]); + + let expression = create_derived(context.state, block, node.metadata.expression.has_await); if (dev) { expression = b.call('$.tag', expression, b.literal('[@const]')); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js index abdbc381d99c..f33febeeb281 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js @@ -46,9 +46,6 @@ export function LetDirective(node, context) { read: (node) => b.call('$.get', node) }; - return b.const( - name, - create_derived(context.state, b.thunk(b.member(b.id('$$slotProps'), node.name))) - ); + return b.const(name, create_derived(context.state, b.member(b.id('$$slotProps'), node.name))); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js index 98894780e1fa..895522d47ab2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js @@ -81,7 +81,7 @@ export function SnippetBlock(node, context) { ? b.call( '$.wrap_snippet', b.id(context.state.analysis.name), - has_await ? b.async(b.function(null, args, body)) : b.function(null, args, body) + b.function(null, args, body, has_await) ) : b.arrow(args, body, has_await); diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index defdb1642e1d..56a5f31ffe82 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -588,14 +588,14 @@ export function method(kind, key, params, body, computed = false, is_static = fa * @param {ESTree.BlockStatement} body * @returns {ESTree.FunctionExpression} */ -function function_builder(id, params, body) { +function function_builder(id, params, body, async = false) { return { type: 'FunctionExpression', id, params, body, generator: false, - async: false, + async, metadata: /** @type {any} */ (null) // should not be used by codegen }; } From 88e4fb65282decdeb865c52025d240d5bd4f8198 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Wed, 6 Aug 2025 10:24:27 -0700 Subject: [PATCH 17/18] apply suggestion from review Co-authored-by: Rich Harris --- .../phases/3-transform/client/visitors/AwaitExpression.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js index 7acd5aaaae12..803d317ad40e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js @@ -15,11 +15,7 @@ export function AwaitExpression(node, context) { // preserve context for // a) top-level await and // b) awaits that precede other expressions in template or `$derived(...)` - if ( - tla || - (is_reactive_expression(context) && - (!is_last_evaluated_expression(context, node) || context.path.at(-1)?.type === 'ConstTag')) - ) { + if (tla || (is_reactive_expression(context) && !is_last_evaluated_expression(context, node))) { return b.call(b.await(b.call('$.save', argument))); } From 15f3508529fd56179464f82f392168370f285204 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 6 Aug 2025 13:52:56 -0400 Subject: [PATCH 18/18] Update .changeset/brave-baboons-teach.md --- .changeset/brave-baboons-teach.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/brave-baboons-teach.md b/.changeset/brave-baboons-teach.md index ca761018437c..55d7d2a9884f 100644 --- a/.changeset/brave-baboons-teach.md +++ b/.changeset/brave-baboons-teach.md @@ -2,4 +2,4 @@ 'svelte': minor --- -feat: async fragments +feat: allow `await` inside `@const` declarations