Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ace3422
WIP fix async hydration
Rich-Harris Sep 18, 2025
dd7752c
merge main
Rich-Harris Sep 18, 2025
f695376
add renderer.async method
Rich-Harris Sep 18, 2025
85d5946
update tests
Rich-Harris Sep 18, 2025
5f900f2
changeset
Rich-Harris Sep 18, 2025
e6cfb5c
oops
Rich-Harris Sep 18, 2025
795b86e
WIP fix async attributes
Rich-Harris Sep 18, 2025
3744965
fix
Rich-Harris Sep 19, 2025
f911483
fix
Rich-Harris Sep 19, 2025
27e36ab
Merge branch 'async-attributes' into async-hydration
Rich-Harris Sep 19, 2025
3a9309f
all tests passing
Rich-Harris Sep 19, 2025
e8cc9a4
unused
Rich-Harris Sep 19, 2025
180e36c
unused
Rich-Harris Sep 19, 2025
535d6ae
remove_nodes -> skip_nodes
Rich-Harris Sep 19, 2025
6738230
hydration boundaries around slots
Rich-Harris Sep 19, 2025
0b8fd18
merge
Rich-Harris Sep 19, 2025
ccb1516
reorder arguments
Rich-Harris Sep 19, 2025
4cff78f
add select method
Rich-Harris Sep 19, 2025
6963de7
WIP simplify selects
Rich-Harris Sep 19, 2025
6eac9ed
WIP
Rich-Harris Sep 19, 2025
31d3264
simplify
Rich-Harris Sep 19, 2025
b0900ee
renderer.title
Rich-Harris Sep 19, 2025
27f6aa4
delete unused compact method
Rich-Harris Sep 19, 2025
7583642
simplify
Rich-Harris Sep 19, 2025
90ff438
simplify
Rich-Harris Sep 19, 2025
e6689cb
simplify
Rich-Harris Sep 19, 2025
a328def
simplify
Rich-Harris Sep 19, 2025
5904536
fix TODO
Rich-Harris Sep 19, 2025
4bc05de
remove outdated TODO
Rich-Harris Sep 19, 2025
97bab20
remove outdated TODO
Rich-Harris Sep 19, 2025
ff6152c
rename call_child_renderer -> create_child_block
Rich-Harris Sep 19, 2025
4a0bd3f
burrito
Rich-Harris Sep 19, 2025
994583c
add a couple of unit tests
Rich-Harris Sep 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/six-rabbits-pull.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: async hydration
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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' */
Expand All @@ -12,7 +12,8 @@ import {
process_children,
build_template,
build_attribute_value,
call_child_renderer
create_child_block,
PromiseOptimiser
} from './shared/utils.js';

/**
Expand All @@ -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(`</${node.name}>`)
);

// 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;
}

Expand Down Expand Up @@ -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 `<textarea>` value or a contenteditable binding, we only add
// the body if the attribute/binding is falsy
const inner_state = { ...state, template: [], init: [] };
Expand All @@ -209,22 +205,23 @@ export function RegularElement(node, context) {
process_children(trimmed, { ...context, state });
}

if (select_with_value) {
// we need to create a child scope so that the `select_value` only applies children of this select element
// in an async world, we could technically have two adjacent select elements with async children, in which case
// the second element's select_value would override the first element's select_value if the children of the first
// element hadn't resolved prior to hitting the second element.
const elements = state.template.splice(template_start, Infinity);
state.template.push(
call_child_renderer(b.block(build_template(elements)), select_with_value_async)
);
}

if (!node_is_void) {
state.template.push(b.literal(`</${node.name}>`));
}

if (dev) {
state.template.push(b.stmt(b.call('$.pop_element')));
}

if (optimiser.expressions.length > 0) {
context.state.template.push(
create_child_block(
b.block([optimiser.apply(), ...state.init, ...build_template(state.template)]),
true
)
);
} else {
context.state.init.push(...state.init);
context.state.template.push(...state.template);
}
}
Loading
Loading