Skip to content

Commit c34e44f

Browse files
committed
async props
1 parent 5ae974f commit c34e44f

File tree

5 files changed

+86
-21
lines changed

5 files changed

+86
-21
lines changed

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

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement } from 'estree' */
22
/** @import { AST } from '#compiler' */
3-
/** @import { ComponentContext } from '../../types.js' */
3+
/** @import { ComponentContext, MemoizedExpression } from '../../types.js' */
44
import { dev, is_ignored } from '../../../../../state.js';
55
import { get_attribute_chunks, object } from '../../../../../utils/ast.js';
66
import * as b from '../../../../../utils/builders.js';
7-
import { build_bind_this, memoize_expression, validate_binding } from '../shared/utils.js';
7+
import {
8+
build_bind_this,
9+
get_expression_id,
10+
memoize_expression,
11+
validate_binding
12+
} from '../shared/utils.js';
813
import { build_attribute_value } from '../shared/element.js';
914
import { build_event_handler } from './events.js';
1015
import { determine_slot } from '../../../../../utils/slot.js';
16+
import { create_derived } from '../../utils.js';
1117

1218
/**
1319
* @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node
@@ -40,6 +46,12 @@ export function build_component(node, component_name, context, anchor = context.
4046
/** @type {Record<string, Expression[]>} */
4147
const events = {};
4248

49+
/** @type {MemoizedExpression[]} */
50+
const expressions = [];
51+
52+
/** @type {MemoizedExpression[]} */
53+
const async_expressions = [];
54+
4355
/** @type {Property[]} */
4456
const custom_css_props = [];
4557

@@ -115,16 +127,21 @@ export function build_component(node, component_name, context, anchor = context.
115127
(events[attribute.name] ||= []).push(handler);
116128
} else if (attribute.type === 'SpreadAttribute') {
117129
const expression = /** @type {Expression} */ (context.visit(attribute));
118-
if (attribute.metadata.expression.has_state) {
119-
let value = expression;
120130

121-
if (attribute.metadata.expression.has_call) {
122-
const id = b.id(context.state.scope.generate('spread_element'));
123-
context.state.init.push(b.var(id, b.call('$.derived', b.thunk(value))));
124-
value = b.call('$.get', id);
125-
}
126-
127-
props_and_spreads.push(b.thunk(value));
131+
if (attribute.metadata.expression.has_state) {
132+
props_and_spreads.push(
133+
b.thunk(
134+
attribute.metadata.expression.is_async || attribute.metadata.expression.has_call
135+
? b.call(
136+
'$.get',
137+
get_expression_id(
138+
attribute.metadata.expression.is_async ? async_expressions : expressions,
139+
expression
140+
)
141+
)
142+
: expression
143+
)
144+
);
128145
} else {
129146
props_and_spreads.push(expression);
130147
}
@@ -133,10 +150,15 @@ export function build_component(node, component_name, context, anchor = context.
133150
custom_css_props.push(
134151
b.init(
135152
attribute.name,
136-
build_attribute_value(attribute.value, context, (value, metadata) =>
153+
build_attribute_value(attribute.value, context, (value, metadata) => {
137154
// TODO put the derived in the local block
138-
metadata.has_call ? memoize_expression(context.state, value) : value
139-
).value
155+
return metadata.has_call || metadata.is_async
156+
? b.call(
157+
'$.get',
158+
get_expression_id(metadata.is_async ? async_expressions : expressions, value)
159+
)
160+
: value;
161+
}).value
140162
)
141163
);
142164
continue;
@@ -154,7 +176,7 @@ export function build_component(node, component_name, context, anchor = context.
154176
attribute.value,
155177
context,
156178
(value, metadata) => {
157-
if (!metadata.has_state) return value;
179+
if (!metadata.has_state && !metadata.is_async) return value;
158180

159181
// When we have a non-simple computation, anything other than an Identifier or Member expression,
160182
// then there's a good chance it needs to be memoized to avoid over-firing when read within the
@@ -167,7 +189,12 @@ export function build_component(node, component_name, context, anchor = context.
167189
);
168190
});
169191

170-
return should_wrap_in_derived ? memoize_expression(context.state, value) : value;
192+
return should_wrap_in_derived
193+
? b.call(
194+
'$.get',
195+
get_expression_id(metadata.is_async ? async_expressions : expressions, value)
196+
)
197+
: value;
171198
}
172199
);
173200

@@ -420,7 +447,12 @@ export function build_component(node, component_name, context, anchor = context.
420447
};
421448
}
422449

423-
const statements = [...snippet_declarations];
450+
const statements = [
451+
...snippet_declarations,
452+
...expressions.map((memo) =>
453+
b.let(memo.id, create_derived(context.state, b.thunk(memo.expression)))
454+
)
455+
];
424456

425457
if (node.type === 'SvelteComponent') {
426458
const prev = fn;
@@ -457,5 +489,20 @@ export function build_component(node, component_name, context, anchor = context.
457489
statements.push(b.stmt(fn(anchor)));
458490
}
459491

492+
[...async_expressions, ...expressions].forEach((memo, i) => {
493+
memo.id.name = `$${i}`;
494+
});
495+
496+
if (async_expressions.length > 0) {
497+
return b.stmt(
498+
b.call(
499+
'$.async',
500+
anchor,
501+
b.array(async_expressions.map(({ expression }) => b.thunk(expression, true))),
502+
b.arrow([b.id('$$anchor'), ...async_expressions.map(({ id }) => id)], b.block(statements))
503+
)
504+
);
505+
}
506+
460507
return statements.length > 1 ? b.block(statements) : statements[0];
461508
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/** @import { TemplateNode, Value } from '#client' */
2+
3+
import { async_derived } from '../../reactivity/deriveds.js';
4+
import { suspend } from './boundary.js';
5+
6+
/**
7+
* @param {TemplateNode} node
8+
* @param {Array<() => Promise<any>>} expressions
9+
* @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn
10+
*/
11+
export function async(node, expressions, fn) {
12+
// TODO handle hydration
13+
14+
suspend(Promise.all(expressions.map(async_derived))).then((result) => {
15+
fn(node, ...result.exit());
16+
});
17+
}

packages/svelte/src/internal/client/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export {
1414
export { check_target, legacy_api } from './dev/legacy.js';
1515
export { trace } from './dev/tracing.js';
1616
export { inspect } from './dev/inspect.js';
17+
export { async } from './dom/blocks/async.js';
1718
export { await_block as await } from './dom/blocks/await.js';
1819
export { if_block as if } from './dom/blocks/if.js';
1920
export { key_block as key } from './dom/blocks/key.js';
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script>
2-
let { num } = $props();
2+
let { value } = $props();
33
</script>
44

5-
<p>{num}</p>
5+
<h1>{value}</h1>

packages/svelte/tests/runtime-runes/samples/async-prop/_config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export default test({
2222
await Promise.resolve();
2323
await Promise.resolve();
2424
await tick();
25-
assert.htmlEqual(target.innerHTML, '<p>hello</p>');
25+
assert.htmlEqual(target.innerHTML, '<h1>hello</h1>');
2626

2727
d = deferred();
2828
component.promise = d.promise;
@@ -32,6 +32,6 @@ export default test({
3232
d.resolve('hello again');
3333
await Promise.resolve();
3434
await tick();
35-
assert.htmlEqual(target.innerHTML, '<p>hello again</p>');
35+
assert.htmlEqual(target.innerHTML, '<h1>hello again</h1>');
3636
}
3737
});

0 commit comments

Comments
 (0)