diff --git a/.changeset/six-rabbits-pull.md b/.changeset/six-rabbits-pull.md new file mode 100644 index 0000000000..55fc8c89b8 --- /dev/null +++ b/.changeset/six-rabbits-pull.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: async hydration 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 7feeebdbbc..5c8ce897f4 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 @@ -130,7 +130,7 @@ export function build_component(node, component_name, context) { } else if (attribute.type === 'SpreadAttribute') { const expression = /** @type {Expression} */ (context.visit(attribute)); - if (attribute.metadata.expression.has_state) { + if (attribute.metadata.expression.has_state || attribute.metadata.expression.has_await) { props_and_spreads.push( b.thunk( attribute.metadata.expression.has_await || attribute.metadata.expression.has_call 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 994db533c2..90845fb303 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 @@ -40,7 +40,11 @@ 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_child_renderer, call_component_renderer } from './visitors/shared/utils.js'; +import { + create_child_block, + call_component_renderer, + create_async_block +} from './visitors/shared/utils.js'; /** @type {Visitors} */ const global_visitors = { @@ -244,7 +248,7 @@ export function server_component(analysis, options) { ]); if (analysis.instance.has_await) { - component_block = b.block([call_child_renderer(component_block, true)]); + component_block = b.block([create_child_block(component_block, true)]); } // trick esrap into including comments 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 e88f117c5c..132a83ec60 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, call_child_renderer } from './shared/utils.js'; +import { block_close, create_async_block } from './shared/utils.js'; /** * @param {AST.AwaitBlock} node @@ -26,7 +26,7 @@ export function AwaitBlock(node, context) { ); if (node.metadata.expression.has_await) { - statement = call_child_renderer(b.block([statement]), true); + statement = create_async_block(b.block([statement])); } context.state.template.push(statement, 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 a8821c4ee7..5a7ca8b566 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, call_child_renderer } from './shared/utils.js'; +import { block_close, block_open, block_open_else, create_async_block } from './shared/utils.js'; /** * @param {AST.EachBlock} node @@ -64,7 +64,7 @@ export function EachBlock(node, context) { } if (node.metadata.expression.has_await) { - state.template.push(call_child_renderer(block, true), block_close); + state.template.push(create_async_block(block), block_close); } else { state.template.push(...block.body, block_close); } 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 426e468927..798cb02e49 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, call_child_renderer } from './shared/utils.js'; +import { block_close, block_open, block_open_else, create_async_block } from './shared/utils.js'; /** * @param {AST.IfBlock} node @@ -24,7 +24,7 @@ export function IfBlock(node, context) { let statement = b.if(test, consequent, alternate); if (node.metadata.expression.has_await) { - statement = call_child_renderer(b.block([statement]), true); + statement = create_async_block(b.block([statement])); } context.state.template.push(statement, 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 60ec158b53..14f48d1ce7 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,4 +1,4 @@ -/** @import { Expression, Statement } from 'estree' */ +/** @import { Expression } from 'estree' */ /** @import { Location } from 'locate-character' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext, ComponentServerTransformState } from '../types.js' */ @@ -12,7 +12,8 @@ import { process_children, build_template, build_attribute_value, - call_child_renderer + create_child_block, + PromiseOptimiser } from './shared/utils.js'; /** @@ -27,21 +28,38 @@ export function RegularElement(node, context) { ...context.state, namespace, preserve_whitespace: - context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea' + context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea', + init: [], + template: [] }; const node_is_void = is_void(node.name); - context.state.template.push(b.literal(`<${node.name}`)); - const body = build_element_attributes(node, { ...context, state }); - context.state.template.push(b.literal(node_is_void ? '/>' : '>')); // add `/>` for XHTML compliance + const optimiser = new PromiseOptimiser(); + + state.template.push(b.literal(`<${node.name}`)); + const body = build_element_attributes(node, { ...context, state }, optimiser.transform); + state.template.push(b.literal(node_is_void ? '/>' : '>')); // add `/>` for XHTML compliance if ((node.name === 'script' || node.name === 'style') && node.fragment.nodes.length === 1) { - context.state.template.push( + state.template.push( b.literal(/** @type {AST.Text} */ (node.fragment.nodes[0]).data), 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)]), + true + ) + ); + } else { + context.state.init.push(...state.init); + context.state.template.push(...state.template); + } + return; } @@ -77,114 +95,92 @@ export function RegularElement(node, context) { ); } - let select_with_value = false; - let select_with_value_async = false; - const template_start = state.template.length; - - if (node.name === 'select') { - const value = node.attributes.find( + if ( + node.name === 'select' && + node.attributes.some( (attribute) => - (attribute.type === 'Attribute' || attribute.type === 'BindDirective') && - attribute.name === 'value' + ((attribute.type === 'Attribute' || attribute.type === 'BindDirective') && + attribute.name === 'value') || + attribute.type === 'SpreadAttribute' + ) + ) { + const attributes = build_spread_object( + node, + node.attributes.filter( + (attribute) => + attribute.type === 'Attribute' || + attribute.type === 'BindDirective' || + attribute.type === 'SpreadAttribute' + ), + context, + optimiser.transform ); - const spread = node.attributes.find((attribute) => attribute.type === 'SpreadAttribute'); - if (spread) { - select_with_value = true; - select_with_value_async ||= spread.metadata.expression.has_await; - - state.template.push( - b.stmt( - b.assignment( - '=', - b.id('$$renderer.local.select_value'), - b.member( - build_spread_object( - node, - node.attributes.filter( - (attribute) => - attribute.type === 'Attribute' || - attribute.type === 'BindDirective' || - attribute.type === 'SpreadAttribute' - ), - context - ), - 'value', - false, - true - ) - ) - ) + const inner_state = { ...state, template: [], init: [] }; + process_children(trimmed, { ...context, state: inner_state }); + + const fn = b.arrow( + [b.id('$$renderer')], + b.block([...state.init, ...build_template(inner_state.template)]) + ); + + const statement = b.stmt(b.call('$$renderer.select', attributes, fn)); + + if (optimiser.expressions.length > 0) { + context.state.template.push( + create_child_block(b.block([optimiser.apply(), ...state.init, statement]), true) ); - } else if (value) { - select_with_value = true; - - if (value.type === 'Attribute' && value.value !== true) { - select_with_value_async ||= (Array.isArray(value.value) ? value.value : [value.value]).some( - (tag) => tag.type === 'ExpressionTag' && tag.metadata.expression.has_await - ); - } - - const left = b.id('$$renderer.local.select_value'); - if (value.type === 'Attribute') { - state.template.push( - b.stmt(b.assignment('=', left, build_attribute_value(value.value, context))) - ); - } else if (value.type === 'BindDirective') { - state.template.push( - b.stmt( - b.assignment( - '=', - left, - value.expression.type === 'SequenceExpression' - ? /** @type {Expression} */ (context.visit(b.call(value.expression.expressions[0]))) - : /** @type {Expression} */ (context.visit(value.expression)) - ) - ) - ); - } + } else { + context.state.template.push(...state.init, statement); } + + return; } - if ( - node.name === 'option' && - !node.attributes.some( - (attribute) => - attribute.type === 'SpreadAttribute' || - ((attribute.type === 'Attribute' || attribute.type === 'BindDirective') && - attribute.name === 'value') - ) - ) { + if (node.name === 'option') { + const attributes = build_spread_object( + node, + node.attributes.filter( + (attribute) => + attribute.type === 'Attribute' || + attribute.type === 'BindDirective' || + attribute.type === 'SpreadAttribute' + ), + context, + optimiser.transform + ); + + let body; + if (node.metadata.synthetic_value_node) { - state.template.push( - b.stmt( - b.call( - '$.simple_valueless_option', - b.id('$$renderer'), - b.thunk( - node.metadata.synthetic_value_node.expression, - node.metadata.synthetic_value_node.metadata.expression.has_await - ) - ) - ) + body = optimiser.transform( + node.metadata.synthetic_value_node.expression, + node.metadata.synthetic_value_node.metadata.expression ); } else { const inner_state = { ...state, template: [], init: [] }; process_children(trimmed, { ...context, state: inner_state }); - state.template.push( - b.stmt( - b.call( - '$.valueless_option', - b.id('$$renderer'), - b.arrow( - [b.id('$$renderer')], - b.block([...inner_state.init, ...build_template(inner_state.template)]) - ) - ) - ) + + body = b.arrow( + [b.id('$$renderer')], + b.block([...state.init, ...build_template(inner_state.template)]) ); } - } else if (body !== null) { + + const statement = b.stmt(b.call('$$renderer.option', attributes, body)); + + if (optimiser.expressions.length > 0) { + context.state.template.push( + create_child_block(b.block([optimiser.apply(), ...state.init, statement]), true) + ); + } else { + context.state.template.push(...state.init, statement); + } + + return; + } + + if (body !== null) { // if this is a `