diff --git a/.changeset/poor-students-nail.md b/.changeset/poor-students-nail.md new file mode 100644 index 0000000000..cee650c002 --- /dev/null +++ b/.changeset/poor-students-nail.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: properly hydrate already-resolved async blocks 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 d5ce3caaa9..b50a73b8b6 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 @@ -166,6 +166,7 @@ export function client_component(analysis, options) { in_constructor: false, instance_level_snippets: [], module_level_snippets: [], + is_standalone: false, // these are set inside the `Fragment` visitor, and cannot be used until then init: /** @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 4438ec015b..287bf24ac6 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 @@ -83,6 +83,9 @@ export interface ComponentClientTransformState extends ClientTransformState { readonly instance_level_snippets: VariableDeclaration[]; /** Snippets hoisted to the module */ readonly module_level_snippets: VariableDeclaration[]; + + /** True if the current node is a) a component or render tag and b) the sole child of a block */ + readonly is_standalone: boolean; } export type Context = import('zimmerframe').Context; 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 79a443967c..00b0cfaa2e 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 @@ -120,34 +120,35 @@ export function Fragment(node, context) { state.init.unshift(b.var(id, b.call('$.text'))); close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); + } else if (is_standalone) { + // no need to create a template, we can just use the existing block's anchor + process_children(trimmed, () => b.id('$$anchor'), false, { + ...context, + state: { ...state, is_standalone } + }); } else { - if (is_standalone) { - // no need to create a template, we can just use the existing block's anchor - process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state }); - } else { - /** @type {(is_text: boolean) => Expression} */ - const expression = (is_text) => b.call('$.first_child', id, is_text && b.true); - - process_children(trimmed, expression, false, { ...context, state }); + /** @type {(is_text: boolean) => Expression} */ + const expression = (is_text) => b.call('$.first_child', id, is_text && b.true); - let flags = TEMPLATE_FRAGMENT; + process_children(trimmed, expression, false, { ...context, state }); - if (state.template.needs_import_node) { - flags |= TEMPLATE_USE_IMPORT_NODE; - } + let flags = TEMPLATE_FRAGMENT; - if (state.template.nodes.length === 1 && state.template.nodes[0].type === 'comment') { - // special case — we can use `$.comment` instead of creating a unique template - 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)); + if (state.template.needs_import_node) { + flags |= TEMPLATE_USE_IMPORT_NODE; + } - state.init.unshift(b.var(id, b.call(template_name))); - } + if (state.template.nodes.length === 1 && state.template.nodes[0].type === 'comment') { + // special case — we can use `$.comment` instead of creating a unique template + 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)); - close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); + state.init.unshift(b.var(id, b.call(template_name))); } + + close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); } } 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 d14336bb7e..5d39cf2216 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 @@ -85,6 +85,10 @@ export function RenderTag(node, context) { ) ) ); + + if (context.state.is_standalone) { + context.state.init.push(b.stmt(b.call('$.next'))); + } } else { context.state.init.push(statements.length === 1 ? statements[0] : b.block(statements)); } 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 bb72794af8..1d6d3413bf 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 @@ -461,7 +461,7 @@ export function build_component(node, component_name, loc, context) { memoizer.check_blockers(node.metadata.expression); } - const statements = [...snippet_declarations, ...memoizer.deriveds(context.state.analysis.runes)]; + let statements = [...snippet_declarations, ...memoizer.deriveds(context.state.analysis.runes)]; if (is_component_dynamic) { const prev = fn; @@ -515,15 +515,21 @@ export function build_component(node, component_name, loc, context) { const blockers = memoizer.blockers(); if (async_values || blockers) { - return b.stmt( - b.call( - '$.async', - anchor, - blockers, - async_values, - b.arrow([b.id('$$anchor'), ...memoizer.async_ids()], b.block(statements)) + statements = [ + b.stmt( + b.call( + '$.async', + anchor, + blockers, + async_values, + b.arrow([b.id('$$anchor'), ...memoizer.async_ids()], b.block(statements)) + ) ) - ); + ]; + + if (context.state.is_standalone) { + statements.push(b.stmt(b.call('$.next'))); + } } return statements.length > 1 ? b.block(statements) : statements[0]; 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 0e2f68d0f5..b9f1441bff 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,6 @@ 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 } from './visitors/shared/utils.js'; /** @type {Visitors} */ const global_visitors = { @@ -105,7 +104,7 @@ export function server_component(analysis, options) { namespace: options.namespace, preserve_whitespace: options.preserveWhitespace, state_fields: new Map(), - skip_hydration_boundaries: false, + is_standalone: false, is_instance: false }; @@ -260,7 +259,13 @@ export function server_component(analysis, options) { if (should_inject_context) { component_block = b.block([ - call_component_renderer(component_block, dev && b.id(component_name)) + b.stmt( + b.call( + '$$renderer.component', + b.arrow([b.id('$$renderer')], component_block, false), + dev && b.id(component_name) + ) + ) ]); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts index e7a72fb8ad..4912728a1e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts @@ -26,7 +26,8 @@ export interface ComponentServerTransformState extends ServerTransformState { readonly template: Array; readonly namespace: Namespace; readonly preserve_whitespace: boolean; - readonly skip_hydration_boundaries: boolean; + /** True if the current node is a) a component or render tag and b) the sole child of a block */ + readonly is_standalone: boolean; /** Transformed async `{@const }` declarations (if any) and those coming after them */ async_consts?: { id: Identifier; 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 b8d2e42144..84c2a81612 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 @@ -2,7 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import * as b from '#compiler/builders'; -import { block_close, create_async_block } from './shared/utils.js'; +import { block_close, create_child_block } from './shared/utils.js'; /** * @param {AST.AwaitBlock} node @@ -25,13 +25,12 @@ export function AwaitBlock(node, context) { ) ); - if (node.metadata.expression.is_async()) { - statement = create_async_block( - b.block([statement]), + context.state.template.push( + ...create_child_block( + [statement], node.metadata.expression.blockers(), node.metadata.expression.has_await - ); - } - - context.state.template.push(statement, block_close); + ), + block_close + ); } 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 3c0a8c1676..cb5a61d52f 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 @@ -2,7 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import * as b from '#compiler/builders'; -import { block_close, block_open, block_open_else, create_async_block } from './shared/utils.js'; +import { block_close, block_open, block_open_else, create_child_block } from './shared/utils.js'; /** * @param {AST.EachBlock} node @@ -18,8 +18,8 @@ export function EachBlock(node, context) { const array_id = state.scope.root.unique('each_array'); - /** @type {Statement} */ - let block = b.block([b.const(array_id, b.call('$.ensure_array_like', collection))]); + /** @type {Statement[]} */ + let statements = [b.const(array_id, b.call('$.ensure_array_like', collection))]; /** @type {Statement[]} */ const each = []; @@ -53,7 +53,7 @@ export function EachBlock(node, context) { fallback.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open_else))); - block.body.push( + statements.push( b.if( b.binary('!==', b.member(array_id, 'length'), b.literal(0)), b.block([open, for_loop]), @@ -62,19 +62,15 @@ export function EachBlock(node, context) { ); } else { state.template.push(block_open); - block.body.push(for_loop); + statements.push(for_loop); } - if (node.metadata.expression.is_async()) { - state.template.push( - create_async_block( - block, - node.metadata.expression.blockers(), - node.metadata.expression.has_await - ), - block_close - ); - } else { - state.template.push(...block.body, block_close); - } + state.template.push( + ...create_child_block( + statements, + node.metadata.expression.blockers(), + node.metadata.expression.has_await + ), + block_close + ); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js index ef5bd985ae..a6ff33c7ab 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js @@ -28,7 +28,7 @@ export function Fragment(node, context) { init: [], template: [], namespace, - skip_hydration_boundaries: is_standalone, + is_standalone, async_consts: undefined }; 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 3f423fa60d..ee790c3e7c 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,25 +2,24 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import * as b from '#compiler/builders'; -import { block_close, block_open, create_push } from './shared/utils.js'; +import { create_child_block } from './shared/utils.js'; /** * @param {AST.HtmlTag} node * @param {ComponentContext} context */ export function HtmlTag(node, context) { - const expression = /** @type {Expression} */ (context.visit(node.expression)); - const call = b.call('$.html', expression); + const expression = b.call('$.html', /** @type {Expression} */ (context.visit(node.expression))); - const has_await = node.metadata.expression.has_await; - - if (has_await) { - context.state.template.push(block_open); - } - - context.state.template.push(create_push(call, node.metadata.expression, true)); - - if (has_await) { - context.state.template.push(block_close); + if (node.metadata.expression.is_async()) { + context.state.template.push( + ...create_child_block( + [b.stmt(b.call('$$renderer.push', expression))], + node.metadata.expression.blockers(), + node.metadata.expression.has_await + ) + ); + } else { + context.state.template.push(expression); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js index e8418343be..06b1b1e966 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js @@ -2,7 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import * as b from '#compiler/builders'; -import { block_close, block_open, block_open_else, create_async_block } from './shared/utils.js'; +import { block_close, block_open, block_open_else, create_child_block } from './shared/utils.js'; /** * @param {AST.IfBlock} node @@ -23,17 +23,12 @@ export function IfBlock(node, context) { /** @type {Statement} */ let statement = b.if(test, consequent, alternate); - const is_async = node.metadata.expression.is_async(); - - const has_await = node.metadata.expression.has_await; - - if (is_async || has_await) { - statement = create_async_block( - b.block([statement]), + context.state.template.push( + ...create_child_block( + [statement], node.metadata.expression.blockers(), - !!has_await - ); - } - - context.state.template.push(statement, block_close); + node.metadata.expression.has_await + ), + block_close + ); } 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 5b905752f4..1c6bb0a198 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 @@ -1,5 +1,4 @@ /** @import { Expression } from 'estree' */ -/** @import { Location } from 'locate-character' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext, ComponentServerTransformState } from '../types.js' */ /** @import { Scope } from '../../../scope.js' */ @@ -8,13 +7,7 @@ import { dev, locator } from '../../../../state.js'; import * as b from '#compiler/builders'; import { clean_nodes, determine_namespace_for_children } from '../../utils.js'; import { build_element_attributes, prepare_element_spread_object } from './shared/element.js'; -import { - process_children, - build_template, - create_child_block, - PromiseOptimiser, - create_async_block -} from './shared/utils.js'; +import { process_children, build_template, PromiseOptimiser } from './shared/utils.js'; import { is_customizable_select_element } from '../../../nodes.js'; /** @@ -66,17 +59,9 @@ export function RegularElement(node, context) { b.literal(``) ); - // TODO this is a real edge case, would be good to DRY this out - if (optimiser.expressions.length > 0) { - context.state.template.push( - create_child_block( - b.block([optimiser.apply(), ...state.init, ...build_template(state.template)]) - ) - ); - } else { - context.state.init.push(...state.init); - context.state.template.push(...state.template); - } + context.state.template.push( + ...optimiser.render([...state.init, ...build_template(state.template)]) + ); return; } @@ -130,13 +115,7 @@ export function RegularElement(node, context) { const statement = b.stmt(b.call('$$renderer.select', attributes, fn, ...rest)); - if (optimiser.expressions.length > 0) { - context.state.template.push( - create_child_block(b.block([optimiser.apply(), ...state.init, statement])) - ); - } else { - context.state.template.push(...state.init, statement); - } + context.state.template.push(...optimiser.render([...state.init, statement])); return; } @@ -183,13 +162,7 @@ export function RegularElement(node, context) { const statement = b.stmt(b.call('$$renderer.option', attributes, body, ...rest)); - if (optimiser.expressions.length > 0) { - context.state.template.push( - create_child_block(b.block([optimiser.apply(), ...state.init, statement])) - ); - } else { - context.state.template.push(...state.init, statement); - } + context.state.template.push(...optimiser.render([...state.init, statement])); return; } @@ -235,19 +208,9 @@ export function RegularElement(node, context) { } if (optimiser.is_async()) { - let statements = [...state.init, ...build_template(state.template)]; - - if (optimiser.has_await) { - statements = [create_child_block(b.block([optimiser.apply(), ...statements]))]; - } - - const blockers = optimiser.blockers(); - - if (blockers.elements.length > 0) { - statements = [create_async_block(b.block(statements), blockers, false, false)]; - } - - context.state.template.push(...statements); + context.state.template.push( + ...optimiser.render([...state.init, ...build_template(state.template)]) + ); } else { context.state.init.push(...state.init); context.state.template.push(...state.template); 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 6d7cef0d95..fe20e85e1b 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,7 +3,7 @@ /** @import { ComponentContext } from '../types.js' */ import { unwrap_optional } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; -import { create_async_block, empty_comment, PromiseOptimiser } from './shared/utils.js'; +import { empty_comment, PromiseOptimiser } from './shared/utils.js'; /** * @param {AST.RenderTag} node @@ -35,17 +35,9 @@ export function RenderTag(node, context) { ) ); - if (optimiser.is_async()) { - statement = create_async_block( - b.block([optimiser.apply(), statement]), - optimiser.blockers(), - optimiser.has_await - ); - } - - context.state.template.push(statement); + context.state.template.push(...optimiser.render_block([statement])); - if (!context.state.skip_hydration_boundaries) { + if (!context.state.is_standalone) { 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 d0f8e25d02..3cebdf4541 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 @@ -5,7 +5,6 @@ import * as b from '#compiler/builders'; import { build_attribute_value, PromiseOptimiser, - create_async_block, block_open, block_close } from './shared/utils.js'; @@ -65,13 +64,5 @@ export function SlotElement(node, context) { fallback ); - 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); + context.state.template.push(block_open, ...optimiser.render_block([b.stmt(slot)]), block_close); } 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 75ba323e20..398ecc68a2 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 @@ -1,4 +1,3 @@ -/** @import { Location } from 'locate-character' */ /** @import { BlockStatement, Expression, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ @@ -6,12 +5,7 @@ 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_async_block, - create_child_block, - PromiseOptimiser -} from './shared/utils.js'; +import { build_template, create_child_block, PromiseOptimiser } from './shared/utils.js'; /** * @param {AST.SvelteElement} node @@ -67,36 +61,29 @@ export function SvelteElement(node, context) { const attributes = b.block([...state.init, ...build_template(state.template)]); const children = /** @type {BlockStatement} */ (context.visit(node.fragment, state)); - /** @type {Statement} */ - let statement = b.stmt( - b.call( - '$.element', - b.id('$$renderer'), - tag, - attributes.body.length > 0 && b.thunk(attributes), - children.body.length > 0 && b.thunk(children) - ) + statements.push( + ...optimiser.render([ + b.stmt( + b.call( + '$.element', + b.id('$$renderer'), + tag, + attributes.body.length > 0 && b.thunk(attributes), + children.body.length > 0 && b.thunk(children) + ) + ) + ]) ); - if (optimiser.expressions.length > 0) { - statement = create_child_block(b.block([optimiser.apply(), statement])); - } - - statements.push(statement); - if (dev) { 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); + context.state.template.push( + ...create_child_block( + statements, + node.metadata.expression.blockers(), + node.metadata.expression.has_await + ) + ); } 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 cfd9a27d7b..6a2c6eb0be 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 @@ -1,12 +1,7 @@ /** @import { BlockStatement, Expression, Pattern, Property, SequenceExpression, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../../types.js' */ -import { - empty_comment, - build_attribute_value, - create_async_block, - PromiseOptimiser -} from './utils.js'; +import { empty_comment, build_attribute_value, PromiseOptimiser } from './utils.js'; import * as b from '#compiler/builders'; import { is_element_node } from '../../../../nodes.js'; import { dev } from '../../../../../state.js'; @@ -106,10 +101,16 @@ export function build_inline_component(node, expression, context) { } push_prop(b.prop('init', b.key(attribute.name), value)); - } else if (attribute.type === 'BindDirective' && attribute.name !== 'this') { + } else if (attribute.type === 'BindDirective') { // Bindings are a bit special: we don't want to add them to (async) deriveds but we need to check if they have blockers optimiser.check_blockers(attribute.metadata.expression); + if (attribute.name === 'this') { + // bind:this is client-only, but we still need to check for blockers to ensure + // the server generates matching hydration markers if the client wraps in $.async + continue; + } + if (attribute.expression.type === 'SequenceExpression') { const [get, set] = /** @type {SequenceExpression} */ (context.visit(attribute.expression)) .expressions; @@ -325,32 +326,16 @@ export function build_inline_component(node, expression, context) { optimiser.check_blockers(node.metadata.expression); } - const is_async = optimiser.is_async(); - - if (is_async) { - statement = create_async_block( - b.block([ - optimiser.apply(), - dynamic && custom_css_props.length === 0 - ? b.stmt(b.call('$$renderer.push', empty_comment)) - : b.empty, - statement - ]), - optimiser.blockers(), - optimiser.has_await - ); - } else if (dynamic && custom_css_props.length === 0) { - context.state.template.push(empty_comment); - } - - context.state.template.push(statement); + context.state.template.push( + ...optimiser.render_block([ + dynamic && custom_css_props.length === 0 + ? b.stmt(b.call('$$renderer.push', empty_comment)) + : b.empty, + statement + ]) + ); - if ( - !is_async && - !context.state.skip_hydration_boundaries && - custom_css_props.length === 0 && - optimiser.expressions.length === 0 - ) { + if (!optimiser.is_async() && !context.state.is_standalone && custom_css_props.length === 0) { context.state.template.push(empty_comment); } } 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 b02a935ed2..ee14a4d135 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 @@ -81,7 +81,19 @@ export function process_children(nodes, { visit, state }) { flush(); const expression = /** @type {Expression} */ (visit(node.expression)); - state.template.push(create_push(b.call('$.escape', expression), node.metadata.expression)); + + let call = b.call( + '$$renderer.push', + b.thunk(b.call('$.escape', expression), node.metadata.expression.has_await) + ); + + const blockers = node.metadata.expression.blockers(); + + if (blockers.elements.length > 0) { + call = b.call('$$renderer.async', blockers, b.arrow([b.id('$$renderer')], call)); + } + + state.template.push(b.stmt(call)); } else if (node.type === 'Text' || node.type === 'Comment' || node.type === 'ExpressionTag') { sequence.push(node); } else { @@ -262,72 +274,20 @@ export function build_getter(node, state) { } /** - * Creates a `$$renderer.child(...)` expression statement - * @param {BlockStatement | Expression} body - * @returns {Statement} - */ -export function create_child_block(body) { - return b.stmt(b.call('$$renderer.child', b.arrow([b.id('$$renderer')], body, true))); -} - -/** - * Creates a `$$renderer.async(...)` expression statement - * @param {BlockStatement | Expression} body + * @param {Statement[]} statements * @param {ArrayExpression} blockers * @param {boolean} has_await - * @param {boolean} needs_hydration_markers */ -export function create_async_block( - body, - blockers = b.array([]), - has_await = true, - needs_hydration_markers = true -) { - return b.stmt( - b.call( - needs_hydration_markers ? '$$renderer.async_block' : '$$renderer.async', - blockers, - b.arrow([b.id('$$renderer')], body, has_await) - ) - ); -} - -/** - * @param {Expression} expression - * @param {ExpressionMetadata} metadata - * @param {boolean} needs_hydration_markers - * @returns {Expression | Statement} - */ -export function create_push(expression, metadata, needs_hydration_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, - needs_hydration_markers - ); - } - - return statement; +export function create_child_block(statements, blockers, has_await) { + if (blockers.elements.length === 0 && !has_await) { + return statements; } - return expression; -} + const fn = b.arrow([b.id('$$renderer')], b.block(statements), has_await); -/** - * @param {BlockStatement | Expression} body - * @param {Identifier | false} component_fn_id - * @returns {Statement} - */ -export function call_component_renderer(body, component_fn_id) { - return b.stmt( - b.call('$$renderer.component', b.arrow([b.id('$$renderer')], body, false), component_fn_id) - ); + return blockers.elements.length > 0 + ? [b.stmt(b.call('$$renderer.async_block', blockers, fn))] + : [b.stmt(b.call('$$renderer.child_block', fn))]; } /** @@ -373,7 +333,7 @@ export class PromiseOptimiser { } } - apply() { + #apply() { if (this.expressions.length === 0) { return b.empty; } @@ -403,4 +363,38 @@ export class PromiseOptimiser { is_async() { return this.expressions.length > 0 || this.#blockers.size > 0; } + + /** + * @param {Statement[]} statements + * @returns {Statement[]} + */ + render(statements) { + if (!this.is_async()) { + return statements; + } + + const fn = b.arrow( + [b.id('$$renderer')], + b.block([this.#apply(), ...statements]), + this.has_await + ); + + const blockers = this.blockers(); + + return blockers.elements.length > 0 + ? [b.stmt(b.call('$$renderer.async', blockers, fn))] + : [b.stmt(b.call('$$renderer.child', fn))]; + } + + /** + * @param {Statement[]} statements + * @returns {Statement[]} + */ + render_block(statements) { + if (!this.is_async()) { + return statements; + } + + return create_child_block([this.#apply(), ...statements], this.blockers(), this.has_await); + } } diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js index 49f4c1b7d2..62196350bf 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -171,6 +171,15 @@ export class Renderer { return promises; } + /** + * @param {(renderer: Renderer) => MaybePromise} fn + */ + child_block(fn) { + this.#out.push(BLOCK_OPEN); + this.child(fn); + this.#out.push(BLOCK_CLOSE); + } + /** * Create a child renderer. The child renderer inherits the state from the parent, * but has its own content. diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Inner.svelte new file mode 100644 index 0000000000..99f885189b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Inner.svelte @@ -0,0 +1,4 @@ + +
{@render children?.()}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Outer.svelte b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Outer.svelte new file mode 100644 index 0000000000..99f885189b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Outer.svelte @@ -0,0 +1,4 @@ + +
{@render children?.()}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Trigger.svelte b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Trigger.svelte new file mode 100644 index 0000000000..fc434d748e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Trigger.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/_config.js b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/_config.js new file mode 100644 index 0000000000..d77ba45ae4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/_config.js @@ -0,0 +1,10 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['hydrate'], + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '
foo
'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/main.svelte new file mode 100644 index 0000000000..6cfc73ca25 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/main.svelte @@ -0,0 +1,16 @@ + + + + + foo + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/Component.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/Component.svelte new file mode 100644 index 0000000000..9f4e638629 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/Component.svelte @@ -0,0 +1,5 @@ + + +

{message}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/_config.js new file mode 100644 index 0000000000..2e20f83f7f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/_config.js @@ -0,0 +1,14 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['hydrate'], + + ssrHtml: `

item 1

item 2

item 3

`, + html: `

item 1

item 2

item 3

`, + + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

item 1

item 2

item 3

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/main.svelte new file mode 100644 index 0000000000..ae54b63414 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/main.svelte @@ -0,0 +1,10 @@ + + +{#each messages as message} + +{/each} diff --git a/packages/svelte/tests/snapshot/samples/async-const/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-const/_expected/server/index.svelte.js index a726b903bc..03bbc5ba88 100644 --- a/packages/svelte/tests/snapshot/samples/async-const/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-const/_expected/server/index.svelte.js @@ -19,11 +19,7 @@ export default function Async_const($$renderer) { ]); $$renderer.push(`

`); - - $$renderer.async([promises[1]], ($$renderer) => { - $$renderer.push(() => $.escape(b)); - }); - + $$renderer.async([promises[1]], ($$renderer) => $$renderer.push(() => $.escape(b))); $$renderer.push(`

`); } else { $$renderer.push(''); 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 7249fd6e4f..b331875d04 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_block([], async ($$renderer) => { + $$renderer.child_block(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 43fe9414eb..86948b4a3a 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_block([], async ($$renderer) => { + $$renderer.child_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++) { 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 1e7330429a..c69c038973 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_block([], async ($$renderer) => { + $$renderer.child_block(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 1ca24cf81a..1355ba34f0 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_block([], async ($$renderer) => { + $$renderer.child_block(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-top-level-inspect-server/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-top-level-inspect-server/_expected/server/index.svelte.js index eb99a2fe1d..cff5f2d569 100644 --- a/packages/svelte/tests/snapshot/samples/async-top-level-inspect-server/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-top-level-inspect-server/_expected/server/index.svelte.js @@ -6,10 +6,6 @@ export default function Async_top_level_inspect_server($$renderer) { var $$promises = $$renderer.run([async () => data = await Promise.resolve(42),,]); $$renderer.push(`

`); - - $$renderer.async([$$promises[1]], ($$renderer) => { - $$renderer.push(() => $.escape(data)); - }); - + $$renderer.async([$$promises[1]], ($$renderer) => $$renderer.push(() => $.escape(data))); $$renderer.push(`

`); } \ No newline at end of file