Skip to content

Commit 5c09035

Browse files
authored
fix: async hydration (#16797)
* WIP fix async hydration * add renderer.async method * update tests * changeset * oops * WIP fix async attributes * fix * fix * all tests passing * unused * unused * remove_nodes -> skip_nodes * hydration boundaries around slots * reorder arguments * add select method * WIP simplify selects * WIP * simplify * renderer.title * delete unused compact method * simplify * simplify * simplify * simplify * fix TODO * remove outdated TODO * remove outdated TODO * rename call_child_renderer -> create_child_block * burrito * add a couple of unit tests
1 parent 1c84568 commit 5c09035

File tree

34 files changed

+546
-509
lines changed

34 files changed

+546
-509
lines changed

.changeset/six-rabbits-pull.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: async hydration

packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export function build_component(node, component_name, context) {
130130
} else if (attribute.type === 'SpreadAttribute') {
131131
const expression = /** @type {Expression} */ (context.visit(attribute));
132132

133-
if (attribute.metadata.expression.has_state) {
133+
if (attribute.metadata.expression.has_state || attribute.metadata.expression.has_await) {
134134
props_and_spreads.push(
135135
b.thunk(
136136
attribute.metadata.expression.has_await || attribute.metadata.expression.has_call

packages/svelte/src/compiler/phases/3-transform/server/transform-server.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ import { TitleElement } from './visitors/TitleElement.js';
4040
import { UpdateExpression } from './visitors/UpdateExpression.js';
4141
import { VariableDeclaration } from './visitors/VariableDeclaration.js';
4242
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
43-
import { call_child_renderer, call_component_renderer } from './visitors/shared/utils.js';
43+
import {
44+
create_child_block,
45+
call_component_renderer,
46+
create_async_block
47+
} from './visitors/shared/utils.js';
4448

4549
/** @type {Visitors} */
4650
const global_visitors = {
@@ -244,7 +248,7 @@ export function server_component(analysis, options) {
244248
]);
245249

246250
if (analysis.instance.has_await) {
247-
component_block = b.block([call_child_renderer(component_block, true)]);
251+
component_block = b.block([create_child_block(component_block, true)]);
248252
}
249253

250254
// trick esrap into including comments

packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
/** @import { AST } from '#compiler' */
33
/** @import { ComponentContext } from '../types.js' */
44
import * as b from '#compiler/builders';
5-
import { block_close, call_child_renderer } from './shared/utils.js';
5+
import { block_close, create_async_block } from './shared/utils.js';
66

77
/**
88
* @param {AST.AwaitBlock} node
@@ -26,7 +26,7 @@ export function AwaitBlock(node, context) {
2626
);
2727

2828
if (node.metadata.expression.has_await) {
29-
statement = call_child_renderer(b.block([statement]), true);
29+
statement = create_async_block(b.block([statement]));
3030
}
3131

3232
context.state.template.push(statement, block_close);

packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
/** @import { AST } from '#compiler' */
33
/** @import { ComponentContext } from '../types.js' */
44
import * as b from '#compiler/builders';
5-
import { block_close, block_open, block_open_else, call_child_renderer } from './shared/utils.js';
5+
import { block_close, block_open, block_open_else, create_async_block } from './shared/utils.js';
66

77
/**
88
* @param {AST.EachBlock} node
@@ -64,7 +64,7 @@ export function EachBlock(node, context) {
6464
}
6565

6666
if (node.metadata.expression.has_await) {
67-
state.template.push(call_child_renderer(block, true), block_close);
67+
state.template.push(create_async_block(block), block_close);
6868
} else {
6969
state.template.push(...block.body, block_close);
7070
}

packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
/** @import { AST } from '#compiler' */
33
/** @import { ComponentContext } from '../types.js' */
44
import * as b from '#compiler/builders';
5-
import { block_close, block_open, block_open_else, call_child_renderer } from './shared/utils.js';
5+
import { block_close, block_open, block_open_else, create_async_block } from './shared/utils.js';
66

77
/**
88
* @param {AST.IfBlock} node
@@ -24,7 +24,7 @@ export function IfBlock(node, context) {
2424
let statement = b.if(test, consequent, alternate);
2525

2626
if (node.metadata.expression.has_await) {
27-
statement = call_child_renderer(b.block([statement]), true);
27+
statement = create_async_block(b.block([statement]));
2828
}
2929

3030
context.state.template.push(statement, block_close);
Lines changed: 109 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { Expression, Statement } from 'estree' */
1+
/** @import { Expression } from 'estree' */
22
/** @import { Location } from 'locate-character' */
33
/** @import { AST } from '#compiler' */
44
/** @import { ComponentContext, ComponentServerTransformState } from '../types.js' */
@@ -12,7 +12,8 @@ import {
1212
process_children,
1313
build_template,
1414
build_attribute_value,
15-
call_child_renderer
15+
create_child_block,
16+
PromiseOptimiser
1617
} from './shared/utils.js';
1718

1819
/**
@@ -27,21 +28,38 @@ export function RegularElement(node, context) {
2728
...context.state,
2829
namespace,
2930
preserve_whitespace:
30-
context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea'
31+
context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea',
32+
init: [],
33+
template: []
3134
};
3235

3336
const node_is_void = is_void(node.name);
3437

35-
context.state.template.push(b.literal(`<${node.name}`));
36-
const body = build_element_attributes(node, { ...context, state });
37-
context.state.template.push(b.literal(node_is_void ? '/>' : '>')); // add `/>` for XHTML compliance
38+
const optimiser = new PromiseOptimiser();
39+
40+
state.template.push(b.literal(`<${node.name}`));
41+
const body = build_element_attributes(node, { ...context, state }, optimiser.transform);
42+
state.template.push(b.literal(node_is_void ? '/>' : '>')); // add `/>` for XHTML compliance
3843

3944
if ((node.name === 'script' || node.name === 'style') && node.fragment.nodes.length === 1) {
40-
context.state.template.push(
45+
state.template.push(
4146
b.literal(/** @type {AST.Text} */ (node.fragment.nodes[0]).data),
4247
b.literal(`</${node.name}>`)
4348
);
4449

50+
// TODO this is a real edge case, would be good to DRY this out
51+
if (optimiser.expressions.length > 0) {
52+
context.state.template.push(
53+
create_child_block(
54+
b.block([optimiser.apply(), ...state.init, ...build_template(state.template)]),
55+
true
56+
)
57+
);
58+
} else {
59+
context.state.init.push(...state.init);
60+
context.state.template.push(...state.template);
61+
}
62+
4563
return;
4664
}
4765

@@ -77,114 +95,92 @@ export function RegularElement(node, context) {
7795
);
7896
}
7997

80-
let select_with_value = false;
81-
let select_with_value_async = false;
82-
const template_start = state.template.length;
83-
84-
if (node.name === 'select') {
85-
const value = node.attributes.find(
98+
if (
99+
node.name === 'select' &&
100+
node.attributes.some(
86101
(attribute) =>
87-
(attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
88-
attribute.name === 'value'
102+
((attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
103+
attribute.name === 'value') ||
104+
attribute.type === 'SpreadAttribute'
105+
)
106+
) {
107+
const attributes = build_spread_object(
108+
node,
109+
node.attributes.filter(
110+
(attribute) =>
111+
attribute.type === 'Attribute' ||
112+
attribute.type === 'BindDirective' ||
113+
attribute.type === 'SpreadAttribute'
114+
),
115+
context,
116+
optimiser.transform
89117
);
90118

91-
const spread = node.attributes.find((attribute) => attribute.type === 'SpreadAttribute');
92-
if (spread) {
93-
select_with_value = true;
94-
select_with_value_async ||= spread.metadata.expression.has_await;
95-
96-
state.template.push(
97-
b.stmt(
98-
b.assignment(
99-
'=',
100-
b.id('$$renderer.local.select_value'),
101-
b.member(
102-
build_spread_object(
103-
node,
104-
node.attributes.filter(
105-
(attribute) =>
106-
attribute.type === 'Attribute' ||
107-
attribute.type === 'BindDirective' ||
108-
attribute.type === 'SpreadAttribute'
109-
),
110-
context
111-
),
112-
'value',
113-
false,
114-
true
115-
)
116-
)
117-
)
119+
const inner_state = { ...state, template: [], init: [] };
120+
process_children(trimmed, { ...context, state: inner_state });
121+
122+
const fn = b.arrow(
123+
[b.id('$$renderer')],
124+
b.block([...state.init, ...build_template(inner_state.template)])
125+
);
126+
127+
const statement = b.stmt(b.call('$$renderer.select', attributes, fn));
128+
129+
if (optimiser.expressions.length > 0) {
130+
context.state.template.push(
131+
create_child_block(b.block([optimiser.apply(), ...state.init, statement]), true)
118132
);
119-
} else if (value) {
120-
select_with_value = true;
121-
122-
if (value.type === 'Attribute' && value.value !== true) {
123-
select_with_value_async ||= (Array.isArray(value.value) ? value.value : [value.value]).some(
124-
(tag) => tag.type === 'ExpressionTag' && tag.metadata.expression.has_await
125-
);
126-
}
127-
128-
const left = b.id('$$renderer.local.select_value');
129-
if (value.type === 'Attribute') {
130-
state.template.push(
131-
b.stmt(b.assignment('=', left, build_attribute_value(value.value, context)))
132-
);
133-
} else if (value.type === 'BindDirective') {
134-
state.template.push(
135-
b.stmt(
136-
b.assignment(
137-
'=',
138-
left,
139-
value.expression.type === 'SequenceExpression'
140-
? /** @type {Expression} */ (context.visit(b.call(value.expression.expressions[0])))
141-
: /** @type {Expression} */ (context.visit(value.expression))
142-
)
143-
)
144-
);
145-
}
133+
} else {
134+
context.state.template.push(...state.init, statement);
146135
}
136+
137+
return;
147138
}
148139

149-
if (
150-
node.name === 'option' &&
151-
!node.attributes.some(
152-
(attribute) =>
153-
attribute.type === 'SpreadAttribute' ||
154-
((attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
155-
attribute.name === 'value')
156-
)
157-
) {
140+
if (node.name === 'option') {
141+
const attributes = build_spread_object(
142+
node,
143+
node.attributes.filter(
144+
(attribute) =>
145+
attribute.type === 'Attribute' ||
146+
attribute.type === 'BindDirective' ||
147+
attribute.type === 'SpreadAttribute'
148+
),
149+
context,
150+
optimiser.transform
151+
);
152+
153+
let body;
154+
158155
if (node.metadata.synthetic_value_node) {
159-
state.template.push(
160-
b.stmt(
161-
b.call(
162-
'$.simple_valueless_option',
163-
b.id('$$renderer'),
164-
b.thunk(
165-
node.metadata.synthetic_value_node.expression,
166-
node.metadata.synthetic_value_node.metadata.expression.has_await
167-
)
168-
)
169-
)
156+
body = optimiser.transform(
157+
node.metadata.synthetic_value_node.expression,
158+
node.metadata.synthetic_value_node.metadata.expression
170159
);
171160
} else {
172161
const inner_state = { ...state, template: [], init: [] };
173162
process_children(trimmed, { ...context, state: inner_state });
174-
state.template.push(
175-
b.stmt(
176-
b.call(
177-
'$.valueless_option',
178-
b.id('$$renderer'),
179-
b.arrow(
180-
[b.id('$$renderer')],
181-
b.block([...inner_state.init, ...build_template(inner_state.template)])
182-
)
183-
)
184-
)
163+
164+
body = b.arrow(
165+
[b.id('$$renderer')],
166+
b.block([...state.init, ...build_template(inner_state.template)])
185167
);
186168
}
187-
} else if (body !== null) {
169+
170+
const statement = b.stmt(b.call('$$renderer.option', attributes, body));
171+
172+
if (optimiser.expressions.length > 0) {
173+
context.state.template.push(
174+
create_child_block(b.block([optimiser.apply(), ...state.init, statement]), true)
175+
);
176+
} else {
177+
context.state.template.push(...state.init, statement);
178+
}
179+
180+
return;
181+
}
182+
183+
if (body !== null) {
188184
// if this is a `<textarea>` value or a contenteditable binding, we only add
189185
// the body if the attribute/binding is falsy
190186
const inner_state = { ...state, template: [], init: [] };
@@ -209,22 +205,23 @@ export function RegularElement(node, context) {
209205
process_children(trimmed, { ...context, state });
210206
}
211207

212-
if (select_with_value) {
213-
// we need to create a child scope so that the `select_value` only applies children of this select element
214-
// in an async world, we could technically have two adjacent select elements with async children, in which case
215-
// the second element's select_value would override the first element's select_value if the children of the first
216-
// element hadn't resolved prior to hitting the second element.
217-
const elements = state.template.splice(template_start, Infinity);
218-
state.template.push(
219-
call_child_renderer(b.block(build_template(elements)), select_with_value_async)
220-
);
221-
}
222-
223208
if (!node_is_void) {
224209
state.template.push(b.literal(`</${node.name}>`));
225210
}
226211

227212
if (dev) {
228213
state.template.push(b.stmt(b.call('$.pop_element')));
229214
}
215+
216+
if (optimiser.expressions.length > 0) {
217+
context.state.template.push(
218+
create_child_block(
219+
b.block([optimiser.apply(), ...state.init, ...build_template(state.template)]),
220+
true
221+
)
222+
);
223+
} else {
224+
context.state.init.push(...state.init);
225+
context.state.template.push(...state.template);
226+
}
230227
}

0 commit comments

Comments
 (0)