diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index cd44fd998a..7c0943bf7c 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 080239bac0..7f7dfa3c14 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 af7d0307e9..6e9052a028 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 0000000000..4916c8910e --- /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 a56aca9c5f..560d6c67b7 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 e691be169b..30ad2c29e8 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 6d9dac8a33..ff3d439d59 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 803d317ad4..7acd5aaaae 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 34acdd6bb9..d5c5b2ea41 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 0b10c02ffb..abda1c1644 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 203cf62b37..98894780e1 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 058a1a8e66..8cb3657a5a 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),